#ifndef _mainwin_h 	 
#define _mainwin_h

/***********************************************************************************************
 *  MainWin.h
 *  /home/jeff/upp/MyApps/svn/jammit/MainWin.h (UTF-8)
 *  Author: Jeffrey Scott Humphreys
 *  Time of development: Approx. 2009 - 2010
 *  Source Location: /http://code.google.com/p/jammit/source/browse/#svn/trunk%3Fstate%3Dclosed
 *  License: BSD
 *  Porpoise: Free and fast music player, tag editor
 *  Developed originally on Windows XP and Windows 7, converted to Ubuntu 64-bit
 *  Compiler: GCC-4.4, IDE and framework: U++ Ultimate
 *  Dependent on FMOD audio library, postgreSQL 8/9 database.
 **********************************************************************************************/
#define _CRT_SECURE_NO_WARNINGS 

#define LLOG(x)      //LOG(x)

//#define IMAGECLASS TestImg
//#define IMAGEFILE <PainterExamples/Test.iml>
//#include <Draw/iml_header.h>

//#define IMAGECLASS MyImages
//#define IMAGEFILE "jammit/project.iml"

//#include <Draw/iml_header.h> 

#include "shared.h" // Generic cross-project items (resuability)
#include "project_shared.h" // Scope is to all other jammit files, shared layout file, iml icons
#include "PowerWindow.h" // Encapsulates TopWindow, adds configuration control
#include "AboutWin.h" // Base class is PowerWindow (sort of)
#include "AppInfoWin.h" // Information about jammit, sources, authors, versions, commandline
#include "EditOptionsWin.h" // Options page
#include "TagWin.h" // MP3 File Tags page for editing tags
#include "GridCompareWin.h" // Compare the file tags and db tags; only where change could occur are listed.
#include "PowerArray.h" // Our interpretation of ArrayCtrl, (Not really necessary yet)
#include "FilterField.h"
#include "FileHandler.h" // Support low-level drive identification for space usage tracking, archiving, backup/restore
#include "TagHandler.h" // In 2010, I decided my own audio file tag reader.  taglib is crazy complex trying to be super duper.
#include "StringHandler.h"
#include "MySplitter.h" // Splitter with xmlize

//#include "fooid.h" // libfoo!  acoustic fingerprinting

//#include <xiphcomment.h>

#include "FMODHandler.h"

//#include "Win32Utils.h" // AppInstance, stolen from UltimatePlayer (Problems colliding on BOOL)
#include "DbHandler.h"

#include "NotificationHandler.h" // 

// Test: Locking, how does it affect performance?
#define USE_LOCKS

// My creation:  Allows easier cloning of window pages (Must come after local includes)
#undef WINNAME
#define WINNAME MainWin

// Define callback timer ids (up to 80) (See SetTimeCallback)

#define TIMERID_FMODPOLL 2
#define TIMERID_PLAYBACKASYNC 3

// Labels here for consistency between menu name and window title (a pet peeve of mine)
#define LABEL_ABOUTWIN "About Jammit"
#define LABEL_APPINFOWIN "Application Info"
#define LABEL_EDITOPTIONSWIN "Options"
#define LABEL_TAGWIN "Tags"

// Constants to allow Splitter::Zoom to control display of panels from a code, rather than by name (Zoom, NoZoom)
#define SHOW_LEFT_PANE 0
#define SHOW_RIGHT_PANE 1
#define SHOW_BOTH_PANES -1

// Include lower(title) because 1) the index is by lower(title) , and 2) order by has to be by a ordinal position with an SET operation
// s alias = songs_001, ls = list_songs_001
#define SONG_COLUMN_LIST "lower(s.title) as ltitle, s.songid, s.songs_tbl, s.title, s.filepath, s.filesize, s.artistname, s.albumname, s.trackno, " \
		  " s.releaseyear, s.rating, s.genrename, s.lyrics, s.bitrate, s.samplerate, s.lengthms, " \
		  " case when ls.songid is not null then 'Y' else 'N' end as inplaylist "

//#define DB_PATH "C:\\ProgramData\\jammit\\jammit.db3"

// Listing sound formats supported by FMOD
// TODO: Can we read asf if its just wma?
// Support for over 20 file formats. 
char * supportedPlaybackTypes = (char *)"*.mp1 *.mp2 *.mp3 *.fla *.flac *.wma *.ogg *.oga *.wav *.mid *.fsb *.asf *.vag *.dls *.it *.aiff *.aif *.at3 *.xmz *.aifc *.s3i *.vag *.xma *.xm *.sf2 *.act *.ad2 *.dls *.cda *.g721 *.raw *.yadpcm *.snc *.sw";

// Listing file formats supported by taglib (assuming both read and write)
// TODO: Can read FSB tags if we use FMOD reader
// From doc: ID3v1, ID3v2, APE, FLAC, Xiph, iTunes-style MP4 and WMA tag formats. http://developer.kde.org/~wheeler/taglib/api/index.html
// From doc: MP3, MPC(MusePack), FLAC, MP4, ASF, AIFF, WAV, (TTA)TrueAudio, (wv(+wvc)WavPack, Ogg FLAC, Ogg Vorbis and Speex file formats.
char * supportedTagTypes = (char *)"*.ape *.mp1 *.mp2 *.mp3 *.mpc *.mp4 *.fla *.flac *.wma *.ogg *.oga *.wav *.asf *.speex *.it *.aiff *.aif *.aifc";

struct FoundTagInfo {
	String sqlValue;
};

// For PlayFromList: Recenter screen vertically around cursor?
enum VerticalPositionEnum { VERTCURS_RECENTER = 1, VERTCURS_DONOT_RECENTER };

// For PlayFromList: What does the position integer mean?
//============================================================================================================================
enum PlayPositionEnum { PLAYPOS_ABSOLUTE = 1, PLAYPOS_RELATIVE };

//============================================================================================================================
enum DbWorkTypeEnum {
	DBWT_EXTRACT_TAGS_TO_DB = 1
,	DBWT_CLEAN_FILE_NAMES_UPD_DB
};

// Convert clicks and user actions into meaningful work requests.  These 
//============================================================================================================================
enum WorkRequestEnum { WORKREQ_NA,
	WORKREQ_PLAY, WORKREQ_PAUSE, WORKREQ_PLAYNEXT, 
	WORKREQ_PLAYPREVIOUS, WORKREQ_STOP, WORKREQ_MUTE, 
	WORKREQ_SHOWLAST, WORKREQ_SHOWFIRST
};

// Source of request just to give a unique identifier so syncronize work handler can have an idea of what needs to be done
//============================================================================================================================
enum WorkReqSourceEnum {
	REQSRC_NA // Not available
	// Implemented from here down
,	REQSRC_APPMEDIABAR // Controls on main window
,	REQSRC_GRID // User clicked on a song in the grid
,	REQSRC_KEYBOARDMEDIACONTROLS
,	REQSRC_SHUFFLEDIN // Going through playlist, finished last, grabbing next automatically.
	// Not implemented from here down, list in order of what to implement first
,	REQSRC_APPCLOSE
,	REQSRC_KEYCOMBO    // Like CTRL+P or some such
,	REQSRC_REMOTECONTROL
,	REQSRC_DDE
,	REQSRC_SYSTEMSHUTDOWN
,	REQSRC_SYSTEMSTARTUP
,	REQSRC_SILENTBATCH // Command line arguments, play, don't show buttons, etc.  Song is not on grid(0)
,	REQSRC_NETWORK     // Hypothetical
,	REQSRC_SMS // From Blackberry!!
,	REQSRC_POP3 // Email
,	REQSRC_TOUCH
,	REQSRC_WINMEDPLYR
,	REQSRC_ITUNE
,	REQSRC_VOICE
,	REQSRC_SCHEDULER
,	REQSRC_AMBIENT // Picked up some from local microphone
,	REQSRC_HOUSE // House computer
};

// We are traversing the files looking for tags that would be improved by the latest tag info: What do we do with that info?
//============================================================================================================================
enum EnrichTagsActionEnum {
	ENRICHACTN_BROWSEPOPUP
,	ENRICHACTN_BROWSEGRID
,	ENRICHACTN_JUSTDOIT	
};

enum EnrichTagsScopeEnum {
	ENRICHSCOPE_ALL
,	ENRICHSCOPE_WORKQUEUE
,	ENRICHSCOPE_MISSINGARTISTS
};

// What are we doing?  Gives some indication for new work orders as to the current state
//============================================================================================================================
enum WorkStateEnum {
	WORKSTATE_NA // Not available
,	WORKSTATE_NOTHING
,	WORKSTATE_PLAYING
,	WORKSTATE_PAUSED
,	WORKSTATE_STOPPED  // Does it matter why we stopped?
};

// grid columns in order for index
//============================================================================================================================
enum PlayListDefaultColumnEnum {PL_TITLE, PL_FILEPATH, PL_SONGARTIST, PL_SONGRATING, PL_SONGRELEASEYEAR};

// Controls for background list load (stopLoadingListAndGrabNewFilter)
//============================================================================================================================
enum ListLoaderEnum {LL_RESET, LL_LOADINGFIRSTPAGE, LL_LOADINGPAGE, LL_DONELOADINGPAGE, LL_DONELOADING, LL_KILLLOAD};

// Return values from PlayFile
//============================================================================================================================
enum PlayFileEnum { 
	PlayFile_Worked = 1 /* Skip 0 due to ambiguity */, 
	PlayFile_FileNotFound, 
	PlayFile_FileOpenError,
	PlayFile_NoFileNameGiven, 
	PlayFile_UnsupportedType, 
	PlayFile_DecodeError,
	PlayFile_HandlerNotAvailable 
};

enum RuleFilterEnum {
	RULEFILTER_UPPERS = 1,
	RULEFILTER_ALLUPPERS,
	RULEFILTER_ALLLOWERS
};

class WINNAME; // forward declaration

// Control play/ locking in case of weirdness, like clicking play while playing pauses doh!
StaticMutex playLock; 
StaticMutex loadListLock, loadPlayListLock; // TODO: Vector? 1 per open grid

// Active player.  Allows the static keyboard controls and mp3 callback to access the current player easily.  This isn't set until constructed.
WINNAME *activePlayer;

struct TagEnrichmentControl {
	EnrichTagsActionEnum enumEnrichTagsAction; // GUI action, really.  How it interacts with user
	EnrichTagsScopeEnum enumEnrichTagsScope;  // How many rows to affect.
};

// Colorize keeper rows in main list by each column

//============================================================================================================================
struct ItsAKeeperDisplay : Display {
	// Flag different display types; instantiate a class for each
	enum DisplayRowEnum { DISPROW_EXISTS, DISPROW_MISSING, DISPROW_KEEPEREXISTS, DISPROW_KEEPERMISSING };
	DisplayRowEnum disprowstat;
	ItsAKeeperDisplay() {
	}
	ItsAKeeperDisplay(DisplayRowEnum disprowstat) : disprowstat(disprowstat) {
	}
	virtual void PaintBackground(Draw& w, const Rect& r, const Value& q, Color ink, Color paper, dword style) const	{
		//w.DrawRect(r, Blend(SColorMark, paper, 220));
		if(IsType<AttrText>(q)) {
			const AttrText& t = ValueTo<AttrText>(q);
			if(!IsNull(t.paper))
				paper = t.paper;
		}
		// alpha 240 is too light
		// alpha 230 is better
		// alpha 220 is
		if (disprowstat == DISPROW_KEEPEREXISTS || disprowstat == DISPROW_KEEPERMISSING) {
			w.DrawRect(r, Blend(SRed, paper, 210)); // http://www.ultimatepp.org/src$Core$Color$en-us.html , alpha is Blending factor Blended color - (255 - alpha) / 255.0 * c1 + alpha / 255.0 * c2. Bigger the number, less the change
		} else {
			w.DrawRect(r, paper);
		}
	}
	virtual void Paint(Draw& w, const Rect& r, const Value& q, Color ink, Color paper, dword style) const {
		PaintBackground(w,r,q,ink,paper,style);
		WString txt;
		Font font = StdFont();
		if (disprowstat == DISPROW_KEEPERMISSING || disprowstat == DISPROW_MISSING) {
			font.Italic();
		}
		int a = ALIGN_LEFT;
		int x = r.left;
		int width = r.GetWidth();
		if(IsType<AttrText>(q)) {
			const AttrText& t = ValueTo<AttrText>(q);
			txt = t.text;
			if(!IsNull(t.paper))
				paper = t.paper;
			if(!IsNull(t.ink))
				ink = t.ink;
			if(!IsNull(t.normalink) && !(style & (CURSOR|SELECT|READONLY)))
				ink = t.normalink;
			if(!IsNull(t.font))
				font = t.font;
			if(!IsNull(t.align))
				a = t.align;
			if(!IsNull(t.img)) {
				Size isz = t.img.GetSize();
				w.DrawImage(x, r.top + Upp::max((r.Height() - isz.cy) / 2, 0), t.img);
				x += isz.cx + t.imgspc;
			}
		}
		else
			txt = IsString(q) ? q : StdConvert().Format(q);
		Size tsz = GetTLTextSize(txt, font);
		if(a == ALIGN_RIGHT)
			x = r.right - tsz.cx;
		if(a == ALIGN_CENTER)
			x += (width - tsz.cx) / 2;
		int tcy = GetTLTextHeight(txt, font);
		int tt = r.top + Upp::max((r.Height() - tcy) / 2, 0);
		
		// Trims the string to fit in the box
		if(tsz.cx > width) {
			
			int wd = width;
			w.Clip(r.left, r.top, wd, r.GetHeight());
			DrawTLText(w, x, tt, width, txt, font, ink);
			w.End();
			//w.DrawImage(r.left + wd, tt + font.Info().GetAscent() - isz.cy, DrawImg::threedots(), ink);
		}
		else
			DrawTLText(w, x, tt, width, txt, font, ink);
	}

	
};

#include "MainWinIDs.h" // Exported the grid column ids so that the TagWin could extract columns

//AppInstance app("JAMMIT");  // Stolen from UltimatePlayer, references Win32Utils.h, don't know what it does.

// HACK: Need to push to options
const char *DEFAULT_FILE_DIR = "E:\\Music\\"; // TODO: This needs to be a dynamic parameter

// Exception handler shell for Tag enrichment
//============================================================================================================================
class SongFileTagExc : public Exc {
public:
	bool isError; // We are cheating.  By setting this to false we can use the catch to update a positive log entry.  Better than a goto.  I suppose a finally?
	SongFileTagExc(const String& desc) : isError(1), Exc(desc) {}
	SongFileTagExc(const char *desc) : isError(1), Exc(desc) {}
	SongFileTagExc(const char *desc, bool _isError) : isError(_isError), Exc(desc) {}
};

// Track the last request so we may know what is the root cause of the player's behavior
//============================================================================================================================

//============================================================================================================================
class LastWorkRequest {
	bool requestPresent;
	WorkRequestEnum workRequestEnum;
	WorkReqSourceEnum workReqSourceEnum;
	StaticMutex mutex;
public:
	//============================================================================================================================
	LastWorkRequest() : requestPresent(false), workRequestEnum(WORKREQ_NA), workReqSourceEnum(REQSRC_NA) {
	}

	//============================================================================================================================
	void Set(WorkRequestEnum _workRequestEnum, WorkReqSourceEnum _workReqSourceEnum) {
//		Mutex::Lock __(mutex);  // These blow up when GridClick->SyncAction->Set->Mutex::Lock->Mutex::Enter->EnterCriticalSection(&section)
		workRequestEnum = _workRequestEnum;
		workReqSourceEnum = _workReqSourceEnum;
		requestPresent = true;
	}

	//============================================================================================================================
	bool Get(WorkRequestEnum &_workRequestEnum, WorkReqSourceEnum &_workReqSourceEnum) {
//		Mutex::Lock __(mutex);
		if (!requestPresent) {
			_workRequestEnum = WORKREQ_NA;
			_workReqSourceEnum = REQSRC_NA;
		} else {
			_workRequestEnum = workRequestEnum;
			_workReqSourceEnum = workReqSourceEnum;
		}
		return requestPresent;
	}
	
	//============================================================================================================================
	//
	//  Was Specifically Req
	//
	//  Not the best concept.  More important is, was this song specifically requested, or is it a next/prev/random thing?
	//============================================================================================================================
	bool WasSpecificallyReq() {
		switch (workRequestEnum) {
			case WORKREQ_PLAY:
				return true;
				break;
			default:
			    return false;
		}
	}
}; // End class LastWorkRequest


// NOTE that ProgressLayout is in CtrlLib
//============================================================================================================================
class ProgressWin : public WithMyProgressLayout<PowerWindow> {
public:
	//============================================================================================================================
	ProgressWin() {
		CtrlLayout(*this);
	}

	//============================================================================================================================
	void Open() {
		PowerWindow::Open("tagwin");
	}

	//============================================================================================================================
	void Close() {
		PowerWindow::Close("tagwin");
	}

	//============================================================================================================================
	virtual void Xmlize(XmlIO xml) {
		
	}
	
};

//============================================================================================================================
// Main window: Primary list of audio files for playing, editing
//============================================================================================================================
class MainWin /* Assist++ can't read a macro here */ : public WithMainLayout<PowerWindow> {

protected:
	String filePath; // The currently playing song in UTF double-byte format.  We don't use WString since FMOD takes double-byte.  Make sense?

	// Set all my display controls for various presentations (Wish it wasn't so complicated)
	ItsAKeeperDisplay dispRowExists;
	ItsAKeeperDisplay dispRowMissing;
	ItsAKeeperDisplay dispRowKeeperExists;
	ItsAKeeperDisplay dispRowKeeperMissing;
	
	FMODHandler soundHandler; // Sound Manager	
	// Splits!
	MySplitter horizSplitter;
	StaticRect rightPane; // Will be the ArrayCtrl main list of songs
	//ScrollArea rightPane;
	StaticRect leftPane; // Will be the list of playlists

	// Right pane contents
	Label lastOperationTimer; 
	Label gridRowCount;
	PowerArray mainList; // Tried GridCtrl, switched to ArrayCtrl, which is basis for PowerArray
	// Left pane contents
	PowerArray playList; // GridCtrl has better dragging options
	PowerArray *activeList; // List user is expecting his play controls, keyboard and next songs to come from
	ArrayCtrl tagList; // Replaces playlist on left side when we try
	GridCtrl mainGrid;
	
	// Used to populate grid in background from db or file list
	Thread listLoaderThread;
	 
	// indicator to restart list on a new filter (takes ListLoaderEnum)
	volatile Atomic stopLoadingListAndGrabNewFilter; 
	volatile Atomic stopLoadingPlayListAndGrabNewFilter; // Need a vector of these, but how volatile??
	volatile Atomic stopFetchingTags; 
	
	AboutWin aboutWindow; // About box
	AppInfoWin appInfoWindow; // Possibly useful information about the application; registry settings, ini files, exe location
	EditOptionsWin editOptionsWindow; // User configurable options
	ProgressWin progressWindow; // Progress window that hopefully runs quicker than the status, which reacts to the GUILocks
	TagWin tagWindow;
	GridCompareWin gridCompareWindow; // For tags as a group or batch
	
	// Save the handles to the system media controls, I don't know what for.
	int systemPlayOrPauseKey, systemPlayNextKey, systemPlayPreviousKey, systemStopKey, systemMuteKey;
	// Current filter for filterEditor
	WString filter;

	LastWorkRequest lastWorkRequest; // Track the last work request from the user (No longer used)

	// Menu objects
	FrameTop<StaticBarArea> bararea;
	MenuBar menuBar; // Horizontal stack
	ToolBar toolBar;
	ToolButton
		repeatPlayList, shufflePlayList, sortPlayList, fullScreen, speedClick
	,	playPrevious, playing, playNext, stopPlaying, mute;
	StatusBar status; // Links to WhenHelp, but isn't working right now
	Label menuMessage;
	FilterField filterList; // User dynamic "live" filter (case-sensitive)
	ScrollBar hsb; // Trying to create a grid scrollbar across bottom.	
	bool autoSavePlayList; // Save everytime a change is made.  Eases user annoyance factor.

	WorkStateEnum workState;
	bool runFromIDE; // Flag set immediately in constructor when commandline arg[0] = RUNFROMIDE, which is stuffed when IDE debug runs.  This allows me to disable keyboard control capture, which conflicts with each other.
	DbHandler dbHandler;

	// Dedicated connection for right panel.  Each is a cursor that must maintain private state.
	DbSession leftPanelSession, rightPanelSession;
	
	// In the main constructor, the panels are assigned and the connection is assigned to the function area (main, playlist, ect)
	DbSession *mainListSession, *playListSession; 
public:

	//============================================================================================================================
	bool IsShuffle() {
		return shufflePlayList.IsChecked();
	}
	
public:		
	typedef WINNAME CLASSNAME; // Referenced by WhenAction callbacks and such
	//============================================================================================================================
	MainWin() 
	: stopLoadingListAndGrabNewFilter(0)
	, stopLoadingPlayListAndGrabNewFilter(0) 
	, autoSavePlayList(1) // Default to autosave until parameterized
	{
		// First things first: Check for command line
		
		runFromIDE = (CommandLine().GetCount() && CommandLine()[0] == "RUNFROMIDE");
				
		//SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
		UseHomeDirectoryConfig(true); // Force ConfigFile to construct directories to user folder instead of exe folder

		// The sound handler must call us when the state changes, like a song comes to an end.
		soundHandler.InitializeCallback(&SoundHandlerCallback);
		                               
		// Load cross-package key pairs
		LoadVars(PROJECT_INTERNAL_NAME); // Searches for file (jammit.ini) in executable directory.  Should be in user directory

		// Logging.  "R" means logs in release mode, too.
		RTIMING(CAT << "Creating " << QUOTEME(WINNAME));

		// Initialize are fingerprinter at 44 kHz and 2 channels (uses malloc!)
		
		// GUI stuff follows
		constructMainFrame(); // Load Menu and Toolbar on same line, as theIDE does.
		AddFrame(status); // Load status bar at the bottom.  It's not automatically linking to the Menu, however.  Used by play state changes.
		this->Sizeable(true).Zoomable(true);  // Has to be done BEFORE Run().

		// Set icons for machine
		
		this->LargeIcon(MyImages::bigMainIcon); // The Task Bar Icon.  App icon in project.rc
		
		String exeFilePath = GetExeFilePath();
		
		// Detect in a hack-fashion if we are running on a linux install
		
		WString windowTitle = "jammit";
		
		if (exeFilePath.StartsWith("/usr/bin")) {
			this->Icon(MyImages::mainIcon); // This one is required for Linux Gnome lower panel Window Selector, or else you get the default icon
		} else {
			// The dev icon has a yellow star which helps reduce confusion when the deployed version is running and I don't know which one to kill
			this->Icon(MyImages::mainIconDev); // Assume dev if not installed in bin directory
			windowTitle.Cat(" (Dev)");
		}

		Title(windowTitle);
		
		Ctrl::GlobalBackPaintHint(); // Recommended by Mirek to prevent flickering.
		
		// Construct all document panes (no docking support yet)
		this->Add(horizSplitter);
		horizSplitter.Horz(leftPane, rightPane);
		horizSplitter.SetPos(3000);
		horizSplitter.BarWidth(3);  // Wish it was like the file dialog splitter: 1 pixel wide but captures the mouse
		horizSplitter.Zoom(SHOW_BOTH_PANES);
		rightPane.Add(mainList); //.TopPos(40).HSizePos(2,3));
		rightPane.Add(lastOperationTimer); // Last Operation Timer, currently shows time to load first page
		rightPane.Add(gridRowCount); // Set after threaded load completes
		//rightPane.sc.GetTotal()
		leftPane.Add(playList);
		// Add these above the Horiz scrollbar that we know ScrollArea (rightPane) contains. (12 up from bottom
		lastOperationTimer.SetLabel(t_("...")).SetFont(StdFontZ(8)).SetInk(Color(93, 93, 93)).LeftPosZ(8, 80).BottomPosZ(20, 12);
		gridRowCount.SetLabel(t_("...")).SetAlign(ALIGN_RIGHT).SetFont(StdFontZ(8)).SetInk(Color(101, 101, 101)).RightPosZ(8, 73).BottomPosZ(20, 12);

		// Set the active list to default to be the main list
		activeList = &mainList;

		// Main search list configuration
		// PERFORMANCE: DO NOT USE the DoCount()!  Locks up tight
		// Capture grid user actions
		mainList.SizePos().HSizePosZ(1, 3).VSizePosZ(1, 32); // Position over the lastOperationTimer and gridRowCount, and the scrollbar. Invisible without this command.
		ConfigureGrid(mainList);
		mainList.WhenLeftClick = THISBACK2(ListClick, &mainList, 1);
		mainList.WhenLeftDouble = THISBACK2(ListClick, &mainList, 2);
		mainList.WhenBar = THISBACK(MainListContextMenu); // BUG: of sorts, right-clicking changes selection, which is annoying, need to override entire RightDown function
		// TODO: Support dragging files from 
		mainList.WhenDropInsert = THISBACK(DropInsertInGrid); // Support drag and drop
		mainList.WhenDrag = THISBACK(DragFromGrid); // Dragging object from grid
		// TODO: Try and capture hold mouse down

		// Now add a basic playlist on the left pane
		playList.SizePos(); // Invisible without this command
		ConfigureGrid(playList);
		playList.WhenLeftClick = THISBACK2(ListClick, &playList, 1);
		playList.WhenLeftDouble = THISBACK2(ListClick, &playList, 2);
		playList.WhenBar = THISBACK(PlayListContextMenu);
		playList.Removing();
		playList.AskRemove(false); // It's just a playlist
		playList.WhenDropInsert = THISBACK(DropInsertInPlaylist);
		playList.WhenDrop = THISBACK(DropOnPlaylist);
		playList.WhenDrag = THISBACK(DragFromPlaylist);

		// Set up display managers
		
		dispRowExists = ItsAKeeperDisplay(ItsAKeeperDisplay::DISPROW_EXISTS);
		dispRowMissing = ItsAKeeperDisplay(ItsAKeeperDisplay::DISPROW_MISSING);
		dispRowKeeperExists = ItsAKeeperDisplay(ItsAKeeperDisplay::DISPROW_KEEPEREXISTS);
		dispRowKeeperMissing = ItsAKeeperDisplay(ItsAKeeperDisplay::DISPROW_KEEPERMISSING);
		
		// Put FMOD poller in so it calls us back somewhat regularly (ugh) ever 10th of a second. Kill in destructor.
		SetTimeCallback(-100, THISBACK(UpdateFMODCallback), TIMERID_FMODPOLL);
		
		// TODO: Fast grid splitter to let users easily drag up from edge and browse separately

		// Make the 2 main connections here
		
		dbHandler.Connect(leftPanelSession); // Active playlist, "Keepers"
		dbHandler.Connect(rightPanelSession);  // All known songs
		
		// Assign the the 2 functions we know, well 3, initialization, too.
		
		mainListSession = &rightPanelSession;
		playListSession = &leftPanelSession;
		
		// Borrow the mainSession for initialization
		dbHandler.Initialize(*mainListSession);
		// Has to be separate connection to keep traversal state

		// Clone as select session.  You must clone another session if you wish to update to any degree.
		filter = ""; // Set our dynamic user FTS filter.  This is applied to the primary grid.
		// Start thread to fetch a list of files in the default directory (doesn't scan sub directories :()
//		listLoaderThread.Run(THISBACK(LoadListFromDirectory)); // Load directly from windows directory.  Very fast, though it doesn't do subdirectories.
		listLoaderThread.Run(THISBACK(LoadMainListFromDb)); // Load from db
		listLoaderThread.Run(THISBACK(LoadPlayListFromDb)); // Load playlist with different connection.  Two cursors can't reside on the same PostgreSQL connection.
		
		// Capture filter
		filterList.WhenAction = THISBACK(FilterListEdit); // Filters playlist live
		
		// Put a dim, unselectable instruction text in the field
		filterList.NullText("Filter list for...");

		// Single click selects the entire text since usually you want to replace the filter
		filterList.ClickSelect(true);

		// Assign System wide hot keys (trying to get keyboard media controls)
		// Capture the Play/Pause button.  Note that other players will lose it until this app finishes
		// TODO: Check if already captured by another program, even Jammit
		// Don't capture if in IDE mode, let user jam on other copy
		if (!runFromIDE) {
			systemPlayOrPauseKey = Ctrl::RegisterSystemHotKey(65715, callback(SystemPlayOrPauseKey));
			systemPlayNextKey = Ctrl::RegisterSystemHotKey(65712, callback(SystemPlayNextKey));
			systemPlayPreviousKey = Ctrl::RegisterSystemHotKey(65713, callback(SystemPlayPreviousKey));
			systemStopKey = Ctrl::RegisterSystemHotKey(65714, callback(SystemStopKey));
			
			// Note that Mute will mute if you do not capture it and app has focus.
			systemMuteKey = Ctrl::RegisterSystemHotKey(65709, callback(SystemMuteKey));
		}
		
		// Get media control buttons all organized and images correct and enabled/disabled, in-line with activeList
		SyncControlsToPlayState();

		// Update screen metadata;  This could be done in a delayed fashion after list is displayed to cut display time
		gridRowCount.Tip("File count in grid");

		// Check if another MainWin has registered 
		if (activePlayer) {
			MYERR("activePlayer already set while MainWin Constructor trying to set it.");
		}

		// Register this window as being the "active" player for static global keyboard control
		Mutex::Lock __(playLock);
		activePlayer = this;
		
		// TODO: Options screen shows what all externals that affect operations: environment settings, missing codecs, missing dlls
		
		// Display how long it took to get from app start to display, typically around 9/10ths of a second for release version
		// The first load of a system up takes a little longer, which is caused by the directory read
		// Tabbing to and from seems to cause the first new song selection to be delayed.
		
//		lastOperationTimer.SetText(timer.ToString());
		// ToolTip.cpp:: ends after 200ms SetTimeCallback(200, callback(EndShowMode), (void *)EndShowMode);
		// SetTimeCallback(1000, callback(ShowToolTip), (void *)ToolTipHook);
		lastOperationTimer.Tip("Time from application load to when this screen was available.  Under one second is good!");
//		return;
		RLOG("Time from load to mainWin ready: " << lastOperationTimer.GetText() << " on date: " << Format("%`", GetSysDate()));

		LoadFromXMLFile(*this, GetHomeDirFile("jammit.xml")); // Non-binary form of serialization, which retains value over time as more items are added (binary does not)
	}

	// Clean up memory, mp3 player
	//============================================================================================================================
	~MainWin() {
		RTIMING(CAT << "Destroying " QUOTEME(WINNAME));

		// MUST COME BEFORE BEFORE Mutex!!!!!!!!!!!!!!!!!!!!!!!!!! Else will spin!!!!!!!!!!!!!!!!!
		//Thread::ShutdownThreads();
		KillTimeCallback(TIMERID_FMODPOLL);
		// Get a lock so we don't dump the player in the middle of a global media control hot key static call
		Mutex::Lock __(playLock);
		
		// Shutdown our background threads politely;  They should be watching for this signal
    	
    	
		// Disable this as an activePlayer
		if (activePlayer) {
			activePlayer = NULL;
		}
		
		// Remove hot keys BEFORE destroying mp3 player
		if (systemPlayOrPauseKey)
			Ctrl::UnregisterSystemHotKey(systemPlayOrPauseKey);
		
		StoreToFile(*this, ConfigFile("jammit.config"));
		StoreAsXMLFile(*this, GetHomeDirFile("jammit.xml"));
	}

	//============================================================================================================================
	virtual void Xmlize(XmlIO xml) {
		
		// Called from PowerWindow on open, run, or close
		xml ("horzsplit", horizSplitter)
			("mainlist", mainList)
			("playlist", playList)
		;
	}

	//============================================================================================================================
	virtual void Open() {
			PowerWindow::Open("jammit"); // Call PowerWindow to fetch placement, and run Xmlize
	}
	
	// Clean up memory, mp3 player
	//============================================================================================================================
	virtual void Close() {
		// While we are in the Close event, no threads can continue, so a Wait will lock up the system
		// Notify threads to stop, then trigger a callback
		int x = listLoaderThread.GetCount();
		if (x) {
			loadListLock.Enter();
			stopLoadingListAndGrabNewFilter = LL_KILLLOAD; // Kill any active loops
			stopLoadingPlayListAndGrabNewFilter = LL_KILLLOAD;
			stopFetchingTags = LL_KILLLOAD;
			loadListLock.Leave();
			this->ProcessEvents();
			Sleep(100);
			bool huh;
			this->ProcessEvents(&huh);
			PostCallback(THISBACK(Close));
		} else {
			PowerWindow::Close("jammit");  // Won't close unless this is called
		}
	}

	//============================================================================================================================
	void Exit() {
		
		// TODO: Map esc key to Exit.  Does when Cancel button is there, but I don't want a Cancel button.
		Break();
	}

	// Toolbar event handlers; names have to fit the macro output of ADDTOOL xxxxxxxClick	
	//============================================================================================================================
	void shufflePlayListClick() {
		shufflePlayList.Check(!shufflePlayList.IsChecked());
	}
	
	//============================================================================================================================
	void repeatPlayListClick() {
		repeatPlayList.Check(!repeatPlayList.IsChecked());
	}
	
	//============================================================================================================================
	void sortPlayListClick() {
		sortPlayList.Check(!sortPlayList.IsChecked());
		
		if (sortPlayList.IsChecked()) {
		//	grid.Sort();
			PromptOK("Done");
		}
	}

	// BUG: Doesn't work :(	
	//============================================================================================================================
	void fullScreenClick() {
		
		fullScreen.Check(!fullScreen.IsChecked()); // Flip value
		
		if (fullScreen.IsChecked()) {
			this->FullScreen(true);
			this->RefreshLayout();
			this->Refresh();
		} else {
			// normal screen
		}
	}
 
	// When depressed, then single-click plays a song, else it takes a double-click
	//============================================================================================================================
	void speedClickClick() {
		speedClick.Check(!speedClick.IsChecked());
	}
	
	// User mouse interaction handlers (Job is to convert to formal work requests)

	// Play/Pause toggle button (OptionButton)
	// "Play" button is when it is false, or "Not playing, paused"
	// "Pause" button is when it is true, or "Playing"
	//============================================================================================================================
	void playingClick() {
		Mutex::Lock __(playLock);
		SyncAction(WORKREQ_PLAY, REQSRC_APPMEDIABAR);
	}
	
	//============================================================================================================================
	void stopPlayingClick() {
		Mutex::Lock __(playLock);
		SyncAction(WORKREQ_STOP, REQSRC_APPMEDIABAR);
	}

	//============================================================================================================================
	void playNextClick() {
		Mutex::Lock __(playLock);
		SyncAction(WORKREQ_PLAYNEXT, REQSRC_APPMEDIABAR);
	}

	//============================================================================================================================
	void playPreviousClick() {
		Mutex::Lock __(playLock);
		SyncAction(WORKREQ_PLAYPREVIOUS, REQSRC_APPMEDIABAR);
	}

	//============================================================================================================================
	void muteClick() {
		Mutex::Lock __(playLock);
		SyncAction(WORKREQ_MUTE, REQSRC_APPMEDIABAR);
	}

	// Begin Menu Definition (Defined in reverse order)
	// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
	// TODO: Make this configurable by dragging menu options around; context menus, etc.
	// TODO: Lock menu open for managing
				
	//============================================================================================================================
	void constructMainFrame() {
		int menuMessageWidth = HorzLayoutZoom(100); // Width from right side of menu bar
		int menuWidth = HorzLayoutZoom(130);  // Width from left side of menu bar

		menuBar.Transparent();
		toolBar.SetFrame(NullFrame());
		bararea.Add(menuBar.LeftPos(0, menuWidth).VCenterPos(menuBar.GetStdHeight())); // Menu on left
		bararea.Add(toolBar.HSizePos(menuWidth, menuMessageWidth).VCenterPos(toolBar.GetStdHeight())); // Toolbar in middle
		bararea.Add(menuMessage.RightPos(4, menuMessageWidth).VSizePos(2, 3)); // Message on right
		bararea.Height(Upp::max(menuBar.GetStdHeight(), toolBar.GetStdHeight()));
		AddFrame(bararea);
		toolBar.Transparent();
		menuBar.Add("File", THISBACK(FileMenu)); //menuBar.HelpLine("File help line").HelpTopic("File help topic");
		menuBar.Add("Edit", THISBACK(EditMenu));
		menuBar.Add("Help", THISBACK(HelpMenu));
		toolBar.Separator();
		ADDTOOL(toolBar, repeatPlayList, "Go back to first song in list when I reach the end", BOOL_CHECKED);
		ADDTOOL(toolBar, shufflePlayList, "Mix up the songs in the current play list", BOOL_CHECKED);
		ADDTOOL(toolBar, sortPlayList, "Sort ascending by name", BOOL_NOTCHECKED);
		ADDTOOL(toolBar, fullScreen, "Zoom to full screen", BOOL_NOTCHECKED); fullScreen.Key(K_F11); // Fairly standard key
		ADDTOOL(toolBar, speedClick, "Single-click to play a song", BOOL_CHECKED);
		toolBar.Separator();
		// Media controls laid out according to historical tape deck
		ADDTOOL(toolBar, playPrevious, "Play previous song from what is currently playing", BOOL_NOTCHECKED);
		ADDTOOL(toolBar, playing, "Play or pause the song currently selected", BOOL_NOTCHECKED);
		ADDTOOL(toolBar, playNext, "Play the next song in the currently playing playlist", BOOL_NOTCHECKED);
		ADDTOOL(toolBar, stopPlaying, "Stop song if playing", BOOL_NOTCHECKED);
		ADDTOOL(toolBar, mute, "Mute or unmute currently playing song", BOOL_NOTCHECKED);

		toolBar.Add(filterList, 120);
		// TODO: May put this down in with main listing on header?
	}
	
	// File layer 2
	//============================================================================================================================
	void FileMenu(Bar& bar) { // Vertical stack
		//bar.Add("Export to Text File...", THISBACK(ExportToTextFile)).Help("Export current visible play list to a text file.");
		//bar.Add("Merge Subdirectories", THISBACK(MergeFilesFromSubdirectories)).Help("Maintenance: Fetch all subdirectories files into main file to ease pain of management.");
		bar.Add("Load Db from Directory", THISBACK(LoadDbFromDirectoryCallback)).Help("Maintenance: Fetch all file details into the db");
		//bar.Add("Load Grid from Directory", THISBACK(LoadGridFromDirectoryCallback)).Help("Analysis: Run directly against file system to get basic list in case db not updating correctly");
		bar.Add("Enrich Db From Tags - Browse and Popup", THISBACK2(EnrichDbFromTags, ENRICHACTN_BROWSEPOPUP, ENRICHSCOPE_WORKQUEUE));
		bar.Add("Enrich Db From Tags - Browse as Grid", THISBACK2(EnrichDbFromTags, ENRICHACTN_BROWSEGRID, ENRICHSCOPE_WORKQUEUE)).Help("Don't zap db, just enhance fields with ID3 tags (or other types)");
		bar.Add("Enrich Db From Tags - JUST DO IT!", THISBACK2(EnrichDbFromTags, ENRICHACTN_JUSTDOIT, ENRICHSCOPE_WORKQUEUE)).Help("Don't zap db, just enhance fields with ID3 tags (or other types)");
		bar.Add("Populate Lengths", THISBACK(PopulateSongLength)).Help("Populate db length column with FMOD calculations to millisecond level");
		bar.Add("Backup Keepers", THISBACK(BackupKeepers)).Help("Backup Keeper files to safe repository so we don't ever lose them!!!").Tip("Menu tip?");
		bar.Add("Exit", THISBACK(Exit)).Key(K_ALT_F4);
	}

	// Edit layer 2
	//============================================================================================================================
	void EditMenu(Bar& bar) { // Vertical stack
		bar.Add(LABEL_EDITOPTIONSWIN, THISBACK(EditOptions));
	}

	// Help layer 2
	//============================================================================================================================
	void HelpMenu(Bar& bar) { // Vertical stack

		bar.Add(LABEL_ABOUTWIN, THISBACK(About));
		bar.Add(LABEL_APPINFOWIN, THISBACK(AppInfo));
	}

	//============================================================================================================================
	void BuildCommonMenu(PowerArray *selectedList, Bar &bar) {
		bar.Add("Filter for Artist", THISBACK1(FilterForArtist, selectedList)).Help("Filter list for the current artist name");
		bar.Add("View File Tags", THISBACK1(ViewTagDetailForFile, selectedList)).Help("Get ID3V23 tags and display for comparison and editing");
		bar.Add("Copy name(s) to clipboard", THISBACK1(CopyName, selectedList)).Help("Copy the name of the selected file to the clipboard");
		bar.Add("Copy path(s) to clipboard", THISBACK1(CopyPath, selectedList)).Help("Copy the path of the selected file to the clipboard");
		bar.Add("Get Song Length", THISBACK1(GetSongLength, selectedList)).Help("Get length of song using FMOD to scan the file, not tags");
		bar.Add("Filter UPPERs", THISBACK2(FilterForRule, selectedList, RULEFILTER_UPPERS)).Help("Filter the grid for any song names with 2 or more upper-case letters");
		
	}
	
	// Right-click in grid, this pops up (hopefully)
	//============================================================================================================================
	//
	//  Main List Context Menu
	//
	//============================================================================================================================
	void MainListContextMenu(Bar& bar) {
		BuildCommonMenu(&mainList, bar);
		
/*
		bar.Add("Add to Keepers", THISBACK(CopyName)).Help("Add to your list of keepers"); // A checkbox would be nice, like 
		bar.Add("Recycle Bin", THISBACK(CopyName)).Help("Dump to Recycling bin cuz were scared to delete it");
		bar.Add("Delete", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Remove", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Junk song", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Add to playlist", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Play then Return", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("View Info", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Edit Info", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Compress", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Send to...", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Search for Duplicates", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Others by Artist", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Others on Album", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Analyze...", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Apply DSP", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Reverse", THISBACK(CopyName)).Help("Copy the name of the file you clicked on to the clipboard");
		bar.Add("Tail", THISBACK(CopyName)).Help("Play last 10 seconds, like UNIX tail");
		bar.Add("Head", THISBACK(CopyName)).Help("Play first 10 seconds and so on through file list");
		bar.Add("Trim", THISBACK(CopyName)).Help("Scan head and tail for low volume segments that can be trimmed logically");
		bar.Add("Research", THISBACK(CopyName)).Help("Google for lyrics, artist info, next play dates, new recordings");
		bar.Add("View in Explorer", THISBACK(CopyName)).Help("Google for lyrics, artist info, next play dates, new recordings");
		bar.Add("Comment", THISBACK(CopyName)).Help("View/Add/Edit comment on this song");
		bar.Add("YouTube", THISBACK(CopyName)).Help("Search youtube for this song as a video and play");
*/
	}

	//============================================================================================================================
    //
    //  Play List Context Menu
    //
    //  User right-clicked in playlist for context menu
    //
	//============================================================================================================================
	void PlayListContextMenu(Bar& bar) {
		bar.Add("Remove Song", THISBACK(RemoveSongFromPlayList)).Help("Take off list");
		bar.Add("Save List", THISBACK(SavePlayList)).Help("Save this playlist to the db");
		BuildCommonMenu(&playList, bar);
		
/*
		bar.Add("Save List As...", THISBACK(CopyName)).Help("Save this list as a new file and new name and show that list as current");
		bar.Add("Save Copy of List", THISBACK(CopyName)).Help("Save a copy of this list but remain editing in the original list");
		bar.Add("Rename List", THISBACK(CopyName)).Help("Rename the active list in the db and keep open");
		bar.Add("Delete List", THISBACK(CopyName)).Help("Save a copy of this list");
		bar.Add("Next List", THISBACK(CopyName)).Help("Save a copy of this list");
		bar.Add("Previous List", THISBACK(CopyName)).Help("Save a copy of this list");
		bar.Add("Open List...", THISBACK(CopyName)).Help("Select a playlist from the db");
		bar.Add("Recent Lists", THISBACK(CopyName)).Help("Browse recently played lists");
		bar.Add("Export List", THISBACK(CopyName));
		bar.Add("Merge Lists", THISBACK(CopyName));
		bar.Add("Compare Lists", THISBACK(CopyName));
		bar.Add("Move to List", THISBACK(CopyName));
		bar.Add("Search Lists", THISBACK(CopyName));
		bar.Add("Upload List", THISBACK(CopyName));
		bar.Add("Flag Keepers", THISBACK(CopyName));
		bar.Add("Write to CD", THISBACK(CopyName));
		bar.Add("Scan CD", THISBACK(CopyName));
*/
	}
	
	// Edit layer 3: Implementation
	//============================================================================================================================
	void EditOptions() {
		editOptionsWindow.Title(LABEL_EDITOPTIONSWIN);
		if (!editOptionsWindow.IsOpen()) {
			editOptionsWindow.Open();
		}
	}

	// Help layer 3: Implementation
	//============================================================================================================================
	void About() {
		// Modeless window; let user keep open if they like on the side.
		aboutWindow.Title(LABEL_ABOUTWIN);
		if (!aboutWindow.IsOpen()) {
			aboutWindow.Open(); // Should call PowerWindow::Open which reads position info
		}
		
		// Writes to file in Closer
		this->Activate(); //Set main window active so user can scroll to a song without skipping a beat.
	}

	//============================================================================================================================
	void AppInfo() {
		appInfoWindow.Title(LABEL_APPINFOWIN);
		if (!appInfoWindow.IsOpen()) {
			appInfoWindow.Open();
		}
		// Leave window active so tips work without click
	}
						
	// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
	// End Menu Definition

	// File	layer 3: Implementation
	//============================================================================================================================
	void ExportToTextFile() {
		// Dialog
		// TODO: Implement
		PromptOK("Complete!");
		// TODO: Let user view file
	}

	//============================================================================================================================
	void MergeFilesFromSubdirectories() {
		FindFile flist;
		
	}

	//============================================================================================================================
	void LoadDbFromDirectoryCallback() {
		String folderToSearch;
		FileSel fs;
		fs.Multi(true).NoAppModal().EditFileName(true).MkDirOption(); // Let em play, dammit Janet!
		// NICETOHAVE: All standard explorer options: Move directories, Delete, sort, rearrange
		// TODO: Remember last base directory you were at
		// Make a whole support menu: Search for music files
		if (fs.ExecuteSelectDir("Select Folder(s) to load music files into the db from")) {
			int selectionCount = fs.GetCount();
			folderToSearch = fs.GetFile(0) + "/"; // + "\\";
			//LoadDbFromDirectory(String().Cat() << folderToSearch);
			listLoaderThread.Run(THISBACK1(LoadDbFromDirectoryThread, folderToSearch));
		}
	}

	//============================================================================================================================
	void LoadDbFromDirectoryThread(String& fromFolder) {
		String lsongsTableName = dbHandler.songsTableName;
		WaitCursor wc; // Hourglass
//		timer.Reset();
		SqlId SONGS(lsongsTableName);
		DbSession db;
		// substitute your 'username' and 'password' here:

		dbHandler.Connect(db);
		Sql lst(db);
		
		// Fetch folder detail

		if (lst * Select(WHENLASTREAD).From(FOLDERS_001).Where(FOLDERPATH == fromFolder)) {
			// If found, look at the last time it was read.  Check timestamp of requested folder.
			// If read time not set, ok.  Pass to call, update from fetched last datetime
		} else {
			DBERR("Failed reading Folder", lst);
		}
		
		int nextID = 0;
		bool flush = false;
		
		// "DON'T FLUSH!"  (An old Texas joke)
		if (flush) {
			// Flush all songs
			if (lst * Delete(SONGS)) {
				LOG("Deleting all rows from " << lsongsTableName);
				// Reset the sequencer (can't do that in Oracle)
				nextID = 0;
				if (!(lst * Insert(SEQUENCES_001)
					(TABLENAME, lsongsTableName)
					(NEXTID, nextID) 
					)) 
				{
					dbHandler.DOA("Execution failure when trying to reset sequence for songs table after deleting all entries.", 70, lst);
				}
			} else {
				dbHandler.DOA("Compile failure on SQL for song deletion", 60, lst);
			}
		} else {
			if (lst * Select(NEXTID).From(SEQUENCES_001).Where(TABLENAME == lsongsTableName)) {
				if (lst.Fetch()) {
					nextID = lst[NEXTID];
				} else {
					// Due to the possibility that data may exist but the sequence may not, we'll fetch what currently is there and start from after the last key

					if (lst * Select(SqlMax(SONGID)).From(SONGS_001)) {
						if (lst.Fetch()) {
							nextID = lst[0];
							nextID++;
						} else {
							// Due to the possibility that data may exist but the sequence may not, we'll fetch what
							
							nextID = 0; // Assume new since failed to fetch an ID from the sequence table.  After adding a song, the sequence key will be added the sequence table.
						}
					} else {
						nextID = 0; // Assume new since failed to fetch an ID from the sequence table.  After adding a song, the sequence key will be added the sequence table.
					}
				}
			 } else {
			     DBERR("Error fetching NextID from Sequences", lst);
			 }
		}
		
		int addCount = LoadDbFromDirectoryRecursively(lst, fromFolder, nextID, lsongsTableName);
		PromptOK(CAT << "Done loading db from directory, added " << addCount << " songs");
		db.Close();
//		lastOperationTimer.SetText(timer.ToString());
	}
	
	// Need to know whether FAT or NTFS or otherwise.
	// If NTFS: http://support.microsoft.com/kb/299648
	// If NTFS, each time a file is COPIED into a folder, it keeps the same modified date, but gets a new created date
	// Each time a file is MOVED into a folder, it keeps the same modified and created date
	// Each time a folder is 
	// Performance tweaks:
	// 1) Make sure music directory is indexed (YES)
	// 2) Turn off last accessed: fsutil behavior set disablelastaccess 1 (YES)
	// 3) Defragment (WILL DO)
	// 4) Use more partitions, Use more folders (Difficult to design)
	// 5) Turn off 8.3: fsutil behavior set disable8dot3 1 (YES)
	// 6) Raise cluster to 64K (All files > 1MB) (WORKING)
	// 7) Check size of MFT, does it need expanding? 
	// 8) reverse name files? (UNDESIRABLE)
	// 9) FAT32 Paging file
	// Master File Table (MFT) $STANDARD_INFO and $FILE_NAME
	// TODO: Get $OBJECT_ID, type identifier 64, 128-bit global object identifier, that can be used to address the file instead of its name. This allows a file to be found even when its name is changed.
	// on the FAT file system, create time has a resolution of 10 milliseconds, write time has a resolution of 2 seconds, and access time has a resolution of 1 day. The NTFS file system delays updates to the last access time for a file by up to 1 hour after the last access.
	// The NTFS file system stores time values in UTC format, so they are not affected by changes in time zone or daylight saving time. The FAT file system stores time values based on the local time of the computer. For example, a file that is saved at 3:00pm PST in Washington is seen as 6:00pm EST in New York on an NTFS volume, but it is seen as 3:00pm EST in New York on a FAT volume.
	// http://msdn.microsoft.com/en-us/library/ms724290(VS.85).aspx
	// A file time is a 64-bit value that represents the number of 100-nanosecond intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal Time (UTC).
	//============================================================================================================================
	int LoadDbFromDirectoryRecursively(Sql &lst, String& fromFolder, int &nextID, String lsongsTableName) {
		FindFile flist; // TODO: FindFirstFileEx Function Searches a directory for a file or subdirectory with a name and attributes that match those specified.
		// If the string ends with a wildcard, period, or directory name, the user must have access to the root and all subdirectories on the path. In the ANSI version of this function, the name is limited to MAX_PATH characters. To extend this limit to approximately 32,000 wide characters, call the Unicode version of the function and prepend "\\?\" to the path. For more information, see Naming a File.
		// FindExInfoBasic The FindFirstFileEx function does not query the short file name, improving overall enumeration speed.
		// FindExSearchLimitToDirectories This is an advisory flag. If the file system supports directory filtering, the function searches for a file that matches the specified name and is also a directory. If the file system does not support directory filtering, this flag is silently ignored.
		// FIND_FIRST_EX_CASE_SENSITIVE 1 Searches are case-sensitive.
		// FIND_FIRST_EX_LARGE_FETCH 2 Uses a larger buffer for directory queries, which can increase performance of the find operation. Windows Server 2008, Windows Vista, Windows Server 2003, and Windows XP/2000:  This value is not supported until Windows Server 2008 R2 and Windows 7.
		int addCountForThisDir = 0;
		
		ASSERT(!lsongsTableName.IsEmpty());
		SqlId SONGS(lsongsTableName);	
		if (flist.Search(CAT << fromFolder << "*.*")) {
			
			if (flist.GetName() == ".") flist.Next();
			if (flist.GetName() == "..") flist.Next();
			
			while (true) {
				String fileName = flist.GetName();
				String filePath = CAT << fromFolder << fileName;
				if (flist.IsFolder()) {
					addCountForThisDir+= LoadDbFromDirectoryRecursively(lst, filePath.Cat() << "/", nextID, lsongsTableName);
					if (!flist.Next()) break;
					continue;
				}

				// Make sure this file is a music file we can play
				if (!PatternMatchMulti(supportedPlaybackTypes, fileName)) {
					if (!flist.Next()) break;
					continue;
				}
				
//				dword fileAttributes = flist.GetAttributes();
				
				String driveName = GetDrive(filePath);
				String driveUUID = GetDriveUUID(driveName);
				String folderName = GetFileFolder(filePath); 
				int64 fileSize = flist.GetLength();
				String title = fileName; // Just copy filename as default name in case we can't get
				//FileTime whenFileCreated = flist.GetCreationTime();
				FileTime whenFileChanged = flist.GetLastChangeTime(); // the last time an application modified the file
				FileTime whenFileWritten = flist.GetLastWriteTime(); // For a file, the structure specifies when the file was last written to, truncated, or overwritten, for example, when WriteFile or SetEndOfFile are used. The date and time are not updated when file attributes or security descriptors are changed.
				Time whenFileChangedAsTime(whenFileChanged);
				//For a directory, the structure specifies when the directory is created.
				bool isFileReadOnly = flist.IsReadOnly();
				bool isFileSymbolicLink = flist.IsSymLink();
				//bool isFileCompressed = flist.IsCompressed(); WINDOWS only

				int tryCount = 0;
retryInsertSong:
				lst.ClearError();
				lst.Begin();
				tryCount++;
				if (lst * 
					Insert(SONGS)
						(SONGID, nextID)
						(TITLE, title) // For now its all we have, need the MP3 name
						(FILENAME, fileName)
						(DRIVENAME, driveName)
						(FOLDERNAME, folderName) // Should be without the Drive.
						(ORIGINALFILENAME, fileName)
						(SONGS_TBL, lsongsTableName)  // This is different if brought in from another table
						(FILESIZE, fileSize)
						(ORIGINALFILESIZE, fileSize) // For matching back to removed files if trimming has taken place
						(FILEPATH, filePath)
						(MACHINENAME, dbHandler.machineName)
						(MACHINES_TBL, dbHandler.machinesTableName) // TODO: Need network name
						(NETWORKNAME, dbHandler.networkName)
						(NETWORKS_TBL, dbHandler.networksTableName)
						(ISKEEPER, " ")
						(WHENLASTMODIFIED, whenFileChangedAsTime)
						)
				{
					// Queue up new song for MP3 tag update
					if (lst * 
						Insert(SONGS_WORKQUEUE_001)
							(SONGID, nextID)
							(SONGS_TBL, lsongsTableName)
							(LOOKTOSONGS_TBL, lsongsTableName)
							(WORKTYPEID, DBWT_EXTRACT_TAGS_TO_DB)
							)
					{
						lst.Commit();
						addCountForThisDir++;
						nextID++; // Up the id value.  We are assuming that the value in the server was a valid new one for this SONGS_TBL
					}
					else {
						lst.Rollback();
						DBERR("Error inserting songs_workqueue_001", lst);
					}

					//if (nextID > 100) break; // Test limit
				} else {
					lst.Rollback();
					LOG(lst);
					// BUG: Can't get error code
					// TODO: Clean up and figure out which object is which: db, lst, SQL...
					// TODO: Add transactions.  Supposedly sqlite is faster with them.
					// TODO: May want to increase buffers due to size of row content.
					// TODO: Test that UTF8 names are searchable
					// TODO: Consider dropping the indexes and creating later, since we know the directory is unique
					
					String errCodeStrAsHex = lst.GetErrorCodeString();
					
					if (errCodeStrAsHex == "23505") {
						// We have a duplicate collision again, so we need to look at what index is the issue
						// GetLastError text is duplicate key value violates unique constraint `"AKfilepath`_songs`_001`"
						LOG(lst.GetErrorStatement());
						Vector<String> field = Split( lst.GetLastError(), '"');
						String indexName = field[1]; // Grab quoted portion
						LOG(indexName);
						if (indexName.Find("filepath") >= 0) {
							// index is on filepath, which is a valid collision; we do not want the same file in the database twice
							LOG(String() << "Ignoring attempt at inserting duplicate file into songs_001: " << filePath);
						} else if (indexName.Find("pkey") >= 0) {
							if (tryCount == 1) {
								// Duplicate key; fetch last key and update
								if (lst * Select(SqlMax(SONGID)).From(SONGS_001)) {
									if (lst.Fetch()) {
										nextID = lst[0];
										nextID++;
										goto retryInsertSong;
									} else {
										// Due to the possibility that data may exist but the sequence may not, we'll fetch what
										
										nextID = 0; // Assume new since failed to fetch an ID from the sequence table.  After adding a song, the sequence key will be added the sequence table.
									}
								} else {
									nextID = 0; // Assume new since failed to fetch an ID from the sequence table.  After adding a song, the sequence key will be added the sequence table.
								}
							}
						} else {
							DBERR("Error inserting songs_001 (I)", lst);
						}
					} else {
						DBERR("Error inserting songs_001 (II)", lst);
					}
				}
				
				
				if (!flist.Next()) break;
				
				Sleep(5);
				this->Refresh();
			}	
			
			// Write back the next value.  
			if (!(lst * 
				Insert(SEQUENCES_001)
					(TABLENAME, lsongsTableName)
					(NEXTID, nextID)
				)) 
			{
				// Failed to insert; was it because of duplicate sequence name?
				if (lst.GetErrorCodeString() == "23505") {
					lst.ClearError();
					if (!(lst * ::Update(SEQUENCES_001)
					(NEXTID, nextID)
					.Where(TABLENAME == lsongsTableName)
					))
					{
						DBERR("Error updating sequences_001 with last id# for songs_001", lst);
					} 
				} else {
					DBERR("Error inserting sequences_001 with last id# for songs_001", lst);
				}
			}
		}

		return addCountForThisDir;
	}

	// Haven't tested with new columns
	//============================================================================================================================
	void LoadGridFromDirectoryCallback() { // Grid aka list
		String folderToSearch;
		FileSel fs; // TODO: Clone class and add a cut/n/paste line for just pasting a directory in, plus list of recent searches
		// TODO: Can we make this thing faster?  Maybe some windows based hacks?
		// BUG: The noappmodal does not affect behavior.
		fs.Multi(true).NoAppModal().EditFileName(true).MkDirOption(); // Let em play, dammit Janet!
		// NICETOHAVE: All standard explorer options: Move directories, Delete, sort, rearrange
		// TODO: Remember last base directory you were at
		// Make a whole support menu: Search for music files
		if (fs.ExecuteSelectDir("Select Folder(s) to load music files into the db from")) {
			int selectionCount = fs.GetCount();
			folderToSearch = fs.GetFile(0) + "/"; // WINDOWS ONLY + "\\";
			//LoadDbFromDirectory(String().Cat() << folderToSearch);
		}
	}
	//TODO: CopyProgressRoutine Callback Function An application-defined callback function used with the CopyFileEx, MoveFileTransacted, and MoveFileWithProgress functions. It is called when a portion of a copy or move operation is completed. The LPPROGRESS_ROUTINE type defines a pointer to this callback function. CopyProgressRoutine is a placeholder for the application-defined funct	
	//TODO: MoveFileWithProgress Function Moves a file or directory, including its children. You can provide a callback function that receives progress notifications.
	//TODO: SearchPath Function Searches for a specified file in a specified path.

	//============================================================================================================================
	void UpdateProgress() {
//		progressWindow.filePath.SetData("DDDDDDDDDDDDDDDD");
		progressWindow.Sync();
		progressWindow.Refresh();
		progressWindow.Show();
		progressWindow.RefreshParentLayout();
		Sleep(1);
	}
	
	//============================================================================================================================
	//
	//  Fetch Tag Detail For File - Get tag information from selected (one) file using custom parser
	//
	//============================================================================================================================
	void ViewTagDetailForFile(PowerArray *selectedList) {

		tagWindow.Title(LABEL_TAGWIN);
		if (!tagWindow.IsOpen()) {
			// Pass all the tags we have
			// Pass the list so the viewer can traverse the files in the list itself
			tagWindow.dbHandler = &dbHandler; // Give it a db handler to update the db directly
			tagWindow.Open(selectedList); // Pass current grid so it can traverse through songs
		}
	}

	//============================================================================================================================
	//
	//  Backup Keepers - Physically copy all files on the keeper list (left side playlist) to a separate drive.
	//
	//============================================================================================================================
	void BackupKeepers() {
		listLoaderThread.Run(THISBACK(BackupKeepersThread));
	}

	//============================================================================================================================
	void BackupKeepersThread() {
		WaitCursor wc;
		DbSession db;
		// Has to be separate connection to keep traversal state
		dbHandler.Connect(db); // SLOW: Should be able to clone current connection
		
		Sql lst(db);
		Sql update(db);

		// Take the list from the database so we can get the filepath		
		if (!(lst * Select(FILENAME, FILEPATH).From(LIST_SONGS_001)
		)) {
			Exclamation(Format("Error in open: %s, %s", DeQtf(db.GetLastError()), lst.ToString()));
			return;
		}
		
		// http://www.ultimatepp.org/reference$SqlExp$en-us.html
		LOG("Backup Keepers SQL:" << lst);

		String fileName, filePath, lookToSongsTableName; // Where it is currently stored on this system.
		String dbTitle, dbArtistName, dbAlbumName;
		
		// Since this is a db fetch, the first row has to be fetched initially.  Don't do this with a directory fetch, or you'll lose the first row.
		while (lst.Fetch()) {
			// Make sure we respect messages from main thread.
			if (Thread::IsShutdownThreads()) {
				break;
			}
			
			String lfilePath = lst[FILEPATH];
			String lfileName = lst[FILENAME];
			
			
		}
	}
	
	//============================================================================================================================
	//
	//  Populate Song Length - Length is useful in identifying exact duplicates, but the length in the mp3 tag may be incorrect.
	//
	//============================================================================================================================
	void PopulateSongLength() {
		listLoaderThread.Run(THISBACK(PopulateSongLengthThread));
	}

	//============================================================================================================================
	//
	//  Populate Song Length Thread
	//
	// Use FMOD to read file lengths and update database with acurate millisecond timings.
	//============================================================================================================================
	void PopulateSongLengthThread() {
		WaitCursor wc;
		DbSession db;
		// Has to be separate connection to keep traversal state
		dbHandler.Connect(db); // SLOW: Should be able to clone current connection
		
		Sql lst(db);
		Sql update(db);

		SqlId SONGS(dbHandler.songsTableName);	
		if (!(lst * Select(SONGID.Of(SONGS), SONGS_TBL.Of(SONGS), FILENAME, FILEPATH).From(SONGS) //.ForUpdate()
		      .Where(IsNull(LENGTHMS) || LENGTHMS == 0)
		)) {
			Exclamation(Format("Error in open: %s, %s", DeQtf(db.GetLastError()), lst.ToString()));
			return;
		}

		progressWindow.Title("Populate Song in Db - Progress");
		if (!progressWindow.IsOpen()) {
			progressWindow.Sizeable();
			progressWindow.Open(); // Should call PowerWindow::Open which reads position info
			//progressWindow.NoBackPaint();
			progressWindow.Show();
		}

		String songsTableName // Where the id for this song was created.  It may have been migrated into this system and not renumbered yet.
		, fileName, filePath, lookToSongsTableName; // Where it is currently stored on this system.
		
		int x = 0;
		// Since this is a db fetch, the first row has to be fetched initially.  Don't do this with a directory fetch, or you'll lose the first row.
		while (lst.Fetch()) {
			if(Thread::IsShutdownThreads()) {
				break;
			}
			x++;
			String lfilePath = lst[FILEPATH];
			if (lfilePath.IsEmpty()) {
				continue;
			}

			// Convert to PostCallback
			//PostCallback(callback1(UpdateProgress, lfilePath));			
			progressWindow.filePath.SetText(lfilePath);
			progressWindow.Show();
			PostCallback(THISBACK(UpdateProgress));
			Sleep(10);
			unsigned int length = soundHandler.GetLength(lfilePath);
			
			if (length) {
				Value vlength = (int64)length;
				if (!(update * ::Update(SONGS)
					(LENGTHMS, vlength)
					.Where(SONGID == lst[SONGID] && SONGS_TBL == lst[SONGS_TBL])
				))
				{
					LOG("Error updating songs with FMOD length: " << update.GetErrorStatement() << ", " << update << ", FilePath:" << filePath);
				}
			} else {
				LOG("Length is zero for " << lfilePath);
			}
		}
		
		progressWindow.Close();
	}
	//============================================================================================================================
	//
	//  Enrich Db From Tags - Extract tags from file and update the database.
	//
	//  @enumEnrichTagsAction
	//  - ENRICHACTN_BROWSEPOPUP - Popup after each file and let user edit
	//  - ENRICHACTN_BROWSEGRID  - Add all files to a grid that would change and let use decide
	//  - ENRICHACTN_JUSTDOIT    - No user interaction, update database where a change would occur
	//  - ENRICHACTN_MISSNGARTIST - Speed up listing?
	//============================================================================================================================
	void EnrichDbFromTags(EnrichTagsActionEnum enumEnrichTagsAction, EnrichTagsScopeEnum enumEnrichTagsScope) {
		// Threaditize the load.  Seems to run safely while main thread is loading grid, which has a transaction "lock".  Only works in PostgreSql.  Fails in sqlite3, and MySql can't run an external cursor so mute issue.
		// Tested in PostgreSQL 8.4
		listLoaderThread.Run(THISBACK2(EnrichDbFromTagsThread, enumEnrichTagsAction, enumEnrichTagsScope));
	}

	//============================================================================================================================
	//
	//  Tag Enrichment Thread
	//
	//============================================================================================================================
	void EnrichDbFromTagsThread(EnrichTagsActionEnum enumEnrichTagsAction, EnrichTagsScopeEnum enumEnrichTagsScope) {
		
		// Some basic stats
		int rowsProcessed = 0;
		int unsupportedTypes = 0;
		int isOgg = 0, isFlac = 0, isWMA = 0, isWav = 0, isOther = 0;
		int songsUpdated = 0;
		int errorLogAttempts = 0;
		int errorsLoggedSuccessfully = 0;
		int errorsUpdateOverrun = 0; // This should always be zero
		int errorsLogging = 0; // Update to log failed
		int errorsUpdatingSongs = 0;
		
		// Tags changed
		
		int tagsUpdated = 0;
		int dbMissingTitle = 0;
		int fileFixedDbTitle = 0;
		int dbMissingArtistName = 0;
		int fileFixedDbArtistName = 0;
		int dbMissingAlbumName = 0;
		int fileFixedDbAlbumName = 0;
		int dbMissingGenreName = 0;
		int fileFixedDbGenreName = 0;
		
		WaitCursor wc;
		DbSession db;
		// Has to be separate connection to keep traversal state
		dbHandler.Connect(db); // SLOW: Should be able to clone current connection
		
		Sql lst(db);
		Sql update(db);
		
		SqlId SONGS(dbHandler.songsTableName);	
		
		stopFetchingTags = LL_RESET;
		
		if (enumEnrichTagsScope == ENRICHSCOPE_ALL ) {
			if (!(lst * Select(SONGID, SONGS, FILENAME, FILEPATH, TITLE, ARTISTNAME, ALBUMNAME, GENRENAME).From(SONGS))) {
				Exclamation(Format("Error in open: %s, %s", DeQtf(db.GetLastError()), lst.ToString()));
				return;
			}
		} else {
			if (!(lst * Select(SONGID.Of(SONGS), SONGS_TBL.Of(SONGS), FILENAME, FILEPATH, TITLE, ARTISTNAME, ALBUMNAME, GENRENAME).From(SONGS)
				.InnerJoin(SONGS_WORKQUEUE_001).On(SONGID.Of(SONGS) == SONGID.Of(SONGS_WORKQUEUE_001) 
				&& SONGS_TBL.Of(SONGS) == SONGS_TBL.Of(SONGS_WORKQUEUE_001))
				.Where(ISBEINGWORKEDON.Of(SONGS_WORKQUEUE_001) == "N" 
				&& ISCOMPLETE.Of(SONGS_WORKQUEUE_001) == "N" 
				&& WORKTYPEID.Of(SONGS_WORKQUEUE_001) == DBWT_EXTRACT_TAGS_TO_DB
			))) {
				Exclamation(Format("Error in open: %s, %s", DeQtf(db.GetLastError()), lst.ToString()));
				return;
			}
		}
		
		// http://www.ultimatepp.org/reference$SqlExp$en-us.html
		LOG("Enrichment SQL:" << lst);

		String fileName, filePath, lookToSongsTableName; // Where it is currently stored on this system.
		String dbTitle, dbArtistName, dbAlbumName;
		
		// Since this is a db fetch, the first row has to be fetched initially.  Don't do this with a directory fetch, or you'll lose the first row.
		while (lst.Fetch()) {
			// Make sure we respect messages from main thread.
			if (Thread::IsShutdownThreads()) {
				break;
			}
	
			if (stopFetchingTags == LL_KILLLOAD) {
				break;
			}
			
			rowsProcessed++;
			String fileName = lst[FILENAME];
			ToUpper(fileName);
			if (!TrimRight(fileName).EndsWith(".MP3")) {
				unsupportedTypes++;
				if (TrimRight(fileName).EndsWith(".FLAC")) {
					isFlac++;
				} else if (TrimRight(fileName).EndsWith(".WMA")) {
					isWMA++;
				} else if (TrimRight(fileName).EndsWith(".OGG")) {
					isOgg++;
				} else if (TrimRight(fileName).EndsWith(".WAV")) { // Do WAV files have tags?
					isWav++;
				} else {
					isOther++;
				}
				
				continue;
			}
			int64 songID = lst[SONGID];
			String songsTableName = lst[SONGS_TBL]; // Where the id for this song was created.  It may have been migrated into this system and not renumbered yet.
			String filePath = lst[FILEPATH];
			
			Value dbTitle = lst[TITLE];
			Value dbArtistName = lst[ARTISTNAME];
			Value dbAlbumName = lst[ALBUMNAME];
			Value dbGenreName = lst[GENRENAME];
			
			TagHandler tagHandler;
						
			bool updateTags;
			// Use try/catch to isolate all error logging in one place.  This helps since we have to test for failure at so many points its difficult to know what has been instanced yet.
			try {
				// Call our tag handler to extract tags
				int rtncode = tagHandler.ExtractTags(filePath);
				TagVector extractedTags = tagHandler.extractedTags;
				String tagDump = tagHandler.tagDump;
				String saveSQL; // Save it off to log after update of workqueue completes.
				// Update db song record with fetched tags so we can include these values in a full text search
			
				Value fileTitle = extractedTags.Get(TAG_TITLE, Null);
				Value fileArtistName = extractedTags.Get(TAG_ARTISTNAME, Null);
				Value fileAlbumName = extractedTags.Get(TAG_ALBUMNAME, Null);
				Value fileGenreName = extractedTags.Get(TAG_GENRENAME, Null);
				
				// Some of the file inputs have a null as part of their length!  So we need to trim it by converting it to a char * first, so the NULL will be used and not the embedded length #
				
				String fileTitleStr = TrimBoth(fileTitle.ToString().Begin()); 
				String fileArtistNameStr = TrimBoth(fileArtistName.ToString().Begin()); 
				String fileAlbumNameStr = TrimBoth(fileAlbumName.ToString().Begin());
				String fileGenreNameStr = TrimBoth(fileGenreName.ToString().Begin());
				
				// Convert all the db values to strings
				String dbTitleStr = dbTitle; String dbArtistNameStr = dbArtistName; String dbAlbumNameStr = dbAlbumName;
				String dbGenreNameStr = dbGenreName;
				
				String newTitle, newArtistName, newAlbumName, newGenreName;
				updateTags = false;
				
				// Detect if any fields we are working with would de-null if we use the tags from the file, or if they would change an existing non-null value to a new non-null value
				
				if ((dbTitle.IsNull() && !fileTitle.IsNull())) {
					updateTags = true;
					newTitle = fileTitleStr;
					
				// Compare using string values since Value types are String classes and may include trailing nulls in the comparison
				
				} else if (!dbTitle.IsNull() && !fileTitle.IsNull() && fileTitleStr != dbTitleStr) {	
					updateTags = true;
					newTitle = fileTitleStr;
				} else {
					newTitle = dbTitleStr;
				}
					
				if (dbArtistName.IsNull() && !fileArtistName.IsNull()) {	
					updateTags = true;
					newArtistName = fileArtistNameStr;
					
					
				} else if (!dbArtistName.IsNull() && !fileArtistName.IsNull() && fileArtistNameStr != dbArtistNameStr) {	
					updateTags = true;
					newArtistName = fileArtistNameStr;
				} else {
					newArtistName = dbArtistNameStr;
				}
				
				if (dbAlbumName.IsNull() && !fileAlbumName.IsNull()) {
					updateTags = true;
					newAlbumName = fileAlbumNameStr;
				} else {
					newAlbumName = dbAlbumNameStr;
				}
				
				if (dbGenreName.IsNull() && !fileGenreName.IsNull()) {
					updateTags = true;
					newGenreName = fileGenreNameStr;
				} else {
					newGenreName = dbGenreNameStr;
				}
				
				// If no changes would occur, we don't update the database
				
				if (!updateTags) {
					GuiSleep(1);
					status.Set(CAT << "Processing " << rowsProcessed);
					status.ProcessEvents();
					continue;
				}
				
				// A change would occur, so we'll present the old and new values to user and let them edit and decide if they want to update the database
				
				// Build a vector of the database tags since the tag window is somewhat encapsulated and has no database access
				
				TagVector dbTags;
				dbTags.Add(TAG_FILEPATH, filePath);
				dbTags.Add(TAG_TITLE, dbTitleStr);
				dbTags.Add(TAG_ARTISTNAME, dbArtistNameStr);
				dbTags.Add(TAG_ALBUMNAME, dbAlbumNameStr);
				dbTags.Add(TAG_GENRENAME, dbGenreNameStr);
				
				if (enumEnrichTagsAction == ENRICHACTN_BROWSEPOPUP) {
					// Pass the tags we have to the tag window for presentation, including frames we didn't recognize
					tagWindow.Populate(extractedTags, dbTags, tagHandler.extractedUnrecognizedFrames);
					tagWindow.fileTagDump.SetData(tagHandler.tagDump);
					tagWindow.okOp.Show();
					
					// Clean up the screen since we share it with the context menu for viewing a specific grid-based file
					tagWindow.writeToDb.Hide(); // Same as "Ok" button, without the close
					tagWindow.prevFile.Hide(); // prev/next doesn't make sense in this context
					tagWindow.nextFile.Hide();
					tagWindow.cancelOp.SetData("Cancel"); // Make sure label makes sense in this context
					int rtn = tagWindow.RunAppModal();
					tagWindow.Close();
	
					// User doesn't want to change anything
					if (rtn == IDCANCEL) {
						continue;
					}
	
					// Pull changes from tagWindow if modified by user action.  Even if user backs out there change, it stays modified.
	
					if (tagWindow.dbTitle.IsModified()) {
						newTitle = tagWindow.dbTitle.GetData();
					} else if (tagWindow.fileTitle.IsModified()) {
						newTitle = tagWindow.fileTitle.GetData();
					}
					
					if (tagWindow.dbArtistName.IsModified()) {
						newArtistName = tagWindow.dbArtistName.GetData();
					}
	
					if (tagWindow.dbAlbumName.IsModified()) {
						newAlbumName = tagWindow.dbAlbumName.GetData();
					}
	
					if (tagWindow.dbGenreName.IsModified()) {
						newGenreName = tagWindow.dbGenreName.GetData();
					}
				} else if (enumEnrichTagsAction == ENRICHACTN_BROWSEGRID) {
					if (!gridCompareWindow.IsOpen()) {
						gridCompareWindow.lstFile.Clear();
						gridCompareWindow.lstOldDb.Clear();
						gridCompareWindow.lstNewDb.Clear();
						
						gridCompareWindow.Open();
					}
						gridCompareWindow.lstFile.Ready(false);
						gridCompareWindow.lstOldDb.Ready(false);
						gridCompareWindow.lstNewDb.Ready(false);

						gridCompareWindow.lstFile.Add();
						gridCompareWindow.lstOldDb.Add();
						gridCompareWindow.lstNewDb.Add();

						gridCompareWindow.lstFile.Set(IDTitle, fileTitle);
						gridCompareWindow.lstFile.Set(IDArtistName, fileArtistName);
						gridCompareWindow.lstFile.Set(IDAlbumName, fileAlbumName);
						gridCompareWindow.lstFile.Set(IDGenreName, fileGenreName);
						
						gridCompareWindow.lstOldDb.Set(IDTitle, dbTitle);
						gridCompareWindow.lstOldDb.Set(IDArtistName, dbArtistName);
						gridCompareWindow.lstOldDb.Set(IDAlbumName, dbAlbumName);
						gridCompareWindow.lstOldDb.Set(IDGenreName, dbGenreName);

						gridCompareWindow.lstNewDb.Set(IDTitle, newTitle);
						gridCompareWindow.lstNewDb.Set(IDArtistName, newArtistName);
						gridCompareWindow.lstNewDb.Set(IDAlbumName, newAlbumName);
						gridCompareWindow.lstNewDb.Set(IDGenreName, newGenreName);

						gridCompareWindow.lstFile.Ready(true);
						gridCompareWindow.lstOldDb.Ready(true);
						gridCompareWindow.lstNewDb.Ready(true);

						gridCompareWindow.ProcessEvents();
						continue; // Don't commit to database!
				} else {
					;
					// ENRICHACTN_JUSTDOIT
				}
				
				// Begin the process to update the database with the final new values
				
				update.Begin(); // Use a transaction to manage the change in case of corruption
				if (!(update * ::Update(SONGS)
					(TITLE, newTitle)
					(ALBUMNAME, newAlbumName)
					(ARTISTNAME, newArtistName)
					(GENRENAME, newGenreName)
					//(COMPOSERNAME, composerName) // Should separate out values if multiple names.  Can't sort properly or filter for artists on duets.  Need a many-to-one artist table.
					//(PUBLISHERNAME, publisherName)
					//(RATING, rating) // 0 to 255; need to convert for display
					//(RELEASEYEAR, releaseYear)
					//(TRACKNO, cleanTrackNo) // Stripped "of x nof chapters"
					//(LENGTHMS, lengthMs) // Disabled in place of FMOD sourcing of millisecond accuracy.  All songs seem to be 1 to 7 seconds over a minute boundary.
					//(LYRICS, lyrics)
					//( foundTags[ID3FID_CONDUCTOR].sqlValue), I don't think conductors make much difference, but what do I know?
					//( foundTags[ID3FID_LYRICIST].sqlValue ) // Should create and pull from composer field
					//(LANGUAGE, lyricsLanguage) // eng!!!!!
					//(COMMENTS, comments) // Should add to comments? or replace? Answer: If comment instr current comment, then no, else append with date?
					//(BITRATE, bitRate)
					//(SAMPLERATE, sampleRate)
					//(CHANNELS, channels)
					.Where(SONGID == songID && SONGS_TBL == songsTableName))) 
				{
					DBERR(String().Cat() << "Error updating songs with tags from taglib for file<" << DeQtf(filePath) << ">", update);
					throw SongFileTagExc("Updating song failed");
				// end if (!(update * ::Update(LOOKTOSONGS)
				} else {
					int rowsChanged = update.GetRowsProcessed();
					if (rowsChanged == 0) {
						throw SongFileTagExc("Updated nothing");
					} else if (rowsChanged > 1) {
						throw SongFileTagExc("Updated too many"); // Rare, hopefully, unless key changed
					} else {
						update.Commit();
						tagsUpdated++;
						throw SongFileTagExc("Updated Title, Album, Artist, Genre", false); // Simulate error to update workqueue
					}
				} // end of else for if (!(update * ::Update(LOOKTOSONGS)
			} // end try
			catch (SongFileTagExc e) {
				if (e.isError) {
					errorLogAttempts++;
					LOG("Exception: " << e << " on file " << filePath << " and songID=" << songID);
					update.Rollback(); // Assumption that a transaction is 
				}
				
				String isError = (e.isError? "Y": "N");
								
				if (!(update * ::Update(SONGS_WORKQUEUE_001)
					(ISCOMPLETE, "Y")
					(ISERROR, isError)
					(OUTCOME, e)
					.Where(SONGID == songID && SONGS_TBL == songsTableName) 
				)) {
					DBERR(String().Cat() << "Error updating workqueue update list for songID " << songID << ", songstbl " << songsTableName, update);
					errorsLogging++;
				} else {
					int rowsChanged = update.GetRowsProcessed();
					if (rowsChanged > 1) errorsUpdateOverrun++;
					if (rowsChanged != 1) {
						LOG("Rowcount incorrect: " << rowsChanged << ". Error updating update workqueue item for song without mp3 value: " << update.GetErrorStatement() << ", " << lst << ", FilePath:" << filePath);
						errorsLogging++;
						update.Rollback();
					} else {
						if (e.isError) {
							errorsLoggedSuccessfully++;
						} else {
							songsUpdated++;
							update.Commit();
						}
					}
				}

				Sleep(1); // Give main thread a chance to update.  This makes a difference.  If a string of errors comes, the main GUI will stutter.			
			}
		} // end while (lst.Fetch()) {
		
		String msg;
		msg = "Tag Enrichment complete: Reviewed ";
		msg << rowsProcessed;
		if (tagsUpdated > 0) {
			msg << " Tags Updated " << tagsUpdated;
		}
		status.Set(msg);
	}

	// Override key handler
	//============================================================================================================================
	bool Key(dword key, int x) {
		return PowerWindow::Key(key, x);
	}

	// Global key handler (what makes jammi better than the rest :)
	//============================================================================================================================
	static void SystemPlayOrPauseKey() {
		// Note: Does not grab focus when window is covered (nice), but does make beep
		Mutex::Lock __(playLock);  // Lock to prevent MainWin destructor from destroying mp3 midstream
		if (activePlayer)
			activePlayer->SyncAction(WORKREQ_PLAY, REQSRC_KEYBOARDMEDIACONTROLS);
	}

	//============================================================================================================================
	static void SystemPlayNextKey()	{
		// Note: Does not grab focus when window is covered (nice), but does make beep
		Mutex::Lock __(playLock);  // Lock to prevent MainWin destructor from destroying mp3 midstream
		if (activePlayer)
			activePlayer->SyncAction(WORKREQ_PLAYNEXT, REQSRC_KEYBOARDMEDIACONTROLS);
	}

	//============================================================================================================================
	static void SystemPlayPreviousKey()	{
		// Note: Does not grab focus when window is covered (nice), but does make beep
		// BUG: When 2 apps are open, it gets confused.
		Mutex::Lock __(playLock);  // Lock to prevent MainWin destructor from destroying mp3 midstream
		if (activePlayer)
			activePlayer->SyncAction(WORKREQ_PLAYPREVIOUS, REQSRC_KEYBOARDMEDIACONTROLS);
	}

	//============================================================================================================================
	static void SystemStopKey()	{
		// Note: Does not grab focus when window is covered (nice), but does make beep
		Mutex::Lock __(playLock);  // Lock to prevent MainWin destructor from destroying mp3 midstream
		if (activePlayer)
			activePlayer->SyncAction(WORKREQ_STOP, REQSRC_KEYBOARDMEDIACONTROLS);
	}

	//============================================================================================================================
	static void SystemMuteKey()	{
		Mutex::Lock __(playLock);  // Lock to prevent MainWin destructor from destroying mp3 midstream
		if (activePlayer)
			activePlayer->SyncAction(WORKREQ_MUTE, REQSRC_KEYBOARDMEDIACONTROLS);
	}

	// Playlist drag support and drop.  IsPaste = true if they release the object on them, else its false
	// Supports dragging and dropping onto empty parts of the playlist, which makes it more intuitive.
	//============================================================================================================================
	void DropOnPlaylist(PasteClip& d) {
		int line = playList.GetCount();
		if (AcceptInternal<PowerArray>(d, "array")) {
			playList.InsertDrop(line, d);
			if (autoSavePlayList) {
				SavePlayList(); // Autosave
			}
			playList.SetFocus();
		}
	}

	// Supports dragging and dropping into the populated portion of the playlist
	//============================================================================================================================
	void DropInsertInPlaylist(int line, PasteClip& d) {
		if (AcceptInternal<PowerArray>(d, "array")) {
			playList.InsertDrop(line, d);
			if (autoSavePlayList) {
				SavePlayList(); // Autosave
			}
			playList.SetFocus();
		}
		
		if (AcceptText(d)) {
			playList.Insert(line);
			playList.Set(line, 0, GetString(d));
			playList.SetCursor(line);
			if (autoSavePlayList) {
				SavePlayList(); // Autosave
			}
			playList.SetFocus();
		}
	}

	//============================================================================================================================
	void DragFromPlaylist()	{
		if(playList.DoDragAndDrop(InternalClip(playList, "array")) == DND_MOVE)
			playList.RemoveSelection();
	}

	//============================================================================================================================
	void DropInsertInGrid(int line, PasteClip& d) {
		if(AcceptInternal<PowerArray>(d, "array")) {
			const ArrayCtrl& src = GetInternal<PowerArray>(d);
			bool self = &src == &mainList;
			Vector< Vector<Value> > data;
			for(int i = 0; i < src.GetCount(); i++)
				if(src.IsSel(i)) {
					Value v = src.Get(i, 0);
					data.Add().Add(IsNumber(v) ? FormatIntRoman((int)src.Get(i, 0)) : String(v));
				}
			mainList.InsertDrop(line, data, d, self);
			mainList.SetFocus();
		}
	}

	// If we dragged a song from the grid to an array without holding the Ctrl key down, then remove it from this list.
	//============================================================================================================================
	void DragFromGrid() {
		if(mainList.DoDragAndDrop(InternalClip(mainList, "array"), mainList.GetDragSample()) == DND_MOVE)
			mainList.RemoveSelection();
	}

	// Common grid properties.  Will probably shift to file driven
	//============================================================================================================================
	void ConfigureGrid(ArrayCtrl &grid) {
		ArrangeArray(grid); // Set our standard grid options
		
		// Hard to find option: By setting sizes to absolute instead of ratios, the horizontal scrollbar will automatically appear.
		grid.HeaderObject().Absolute();
		// Using ids (Don't add index!) allows these to be referenced regardless of their position		
		
		// For headers, try to leave room for sort icons
		grid.AddColumn(IDTitle, "Title", 280).Accel() // 250 too small (Only available on ArrayCtrl. Song name, "Accel" runs a keystroke through a filter and does a search.  Default filter is CharFilterDefaultToUpperAscii
			.Margin(0); // Lets try compressing the left edge
		grid.AddColumn(IDArtistName, "Artist", 150); // 2nd most important attribute
		grid.AddColumn(IDAlbumName, "Album", 150);
		grid.AddColumn(IDTrackNo, "Trk", 38); // 35 too small. 40 too big.  Put track after album since it makes more sense.  Some tracks are bizaare +100.
		grid.AddColumn(IDReleaseYear, "Year", 47); // 50 too big, 45 too small.
		grid.AddColumn(IDRating, "Rtg", 40); // 50 too big
		grid.AddColumn(IDGenreName, "Genre", 100);
		grid.AddColumn(IDFileSize, "File Size", 70); // 68 too small // Good for finding dups
		grid.AddColumn(IDLengthMsStr, "Length", 60);
		grid.AddIndex(IDLengthMs);  // Invisible original value for calculating signature
		grid.AddColumn(IDBitRate, "Bit Rate", 60); // Greater than 120 is good
		grid.AddColumn(IDBandName, "Band", 60);
		//grid.AddColumn(IDSampleRate, "Sample Rate", 60);
		grid.AddColumn(IDLyrics, "Lyrics", 300);
		//grid.AddColumn(IDFileType, "Type", 25);
		grid.AddColumn(IDFilePath, "File Path", 250); // Move to the end; become less interesting as tags are more filled in.
		grid.AddColumn(IDSongID, "ID", 30);
		grid.AddColumn(IDSongsTbl, "Tbl", 30);
		grid.AddIndex(IDInPlayList); // true/false for in a play list (colorize row)
	}
public:
	// Our wrappers to allow the mp3 callback to access itself!
	
/*
	int GetPosition(MP3_TIME *pTime) {
		assert(pTime);
		if (mp3) {
			return mp3->GetPosition(pTime);
		} else {
			pTime->ms = -1;
			return 0;
		}
	}
	
	// Be careful: On mp3s this may cause a file scan and delay or interrupt playback
	int GetSongLength(MP3_TIME *pTime) {
		assert(pTime);
		if (mp3) {
			return mp3->GetSongLength(pTime);
		} else {
			pTime->ms = -1;
			return 0;
		}
	}
	
*/
	// GUI Controls for allowing user control over behavior
	
	//============================================================================================================================
	//
	//  Filter List Edit - User entry into live main list filter
	//
	//============================================================================================================================
	void FilterListEdit() {
		loadListLock.Enter();
		filter = filterList.GetData();
		if (filter.GetLength() != 1) { // Only refresh the search if there is either no characters or at least 2.  Single character searches are hard on the database and not very useful.
			stopLoadingListAndGrabNewFilter = LL_RESET; // Set the atomic value and its grabbed by thread that loads the song list.
		}
		//stopLoadingPlayListAndGrabNewFilter = LL_RESET; // Set the atomic value and its grabbed by thread that loads the song list.
		loadListLock.Leave();
	}


	//============================================================================================================================
	//
	//  Filter For Artist - Filter current song list by the just selected or current song's artistname
	//
	//============================================================================================================================
	void FilterForArtist(PowerArray *selectedList) {
		String artistName = selectedList->Get(IDArtistName);
		if (!artistName.IsEmpty()) {
			filterList.SetData(artistName);
			FilterListEdit();
		}
	}

	// BUG: BUG: BUG:
	//============================================================================================================================
	//
	//  Filter for Rule - Check for a rule, like 2 uppercase characters in title.  We filter them down, so we can verify they are valid, like "KISS" as a band name.
	//
	//============================================================================================================================
	void FilterForRule(PowerArray *selectedList, RuleFilterEnum ruleFilter) {
		bool showRow;
		int uppersInARow = 0;
		
		for (int i = 0; i < selectedList->GetCount(); i++) {
			WString title = selectedList->Get(i, IDTitle);
			showRow = false; // Assume it has no double uppers
			const wchar_t *ptr = (wchar_t *)title.GetIter(0);
			const wchar_t *end = (wchar_t *)title.End();
			
			while(ptr <= end) {
				if (isupper(*ptr)) { // BUG: Can't deal with +255 chars
					uppersInARow++;
				} else {
					uppersInARow = 0;
				}
				
				if (uppersInARow > 1) {
					showRow = true; // Show the row in the grid since we found multiple uppers, which may be a corrupt title.
					break;
				}
				ptr++;
			}
			
			if (!showRow) {
				selectedList->SetLineCy(i, 0); // Hide row
			}
		}
	}

	// Probably sunsetting this.  Should be from file
	//============================================================================================================================
	void LoadMainListFromDirectory() {	
		ArrayCtrl &activeList = mainList;
		
		// TODO: filter is case-sensitive:  Any way to undo?
		// TODO: Save in drop list; add to config
		// TODO: Alt-left restores previous filter; edit filters; complex filters?
		FindFile flist;
		stopLoadingListAndGrabNewFilter = LL_RESET;
		// TODO: Add support for lists: M3U, WAX, ASX (AIFF?)
		// TODO: Include AAC (Under GPL)
		// OGG = Vorbis http://en.wikipedia.org/wiki/Vorbis
		// CD Files?
		// Monkey's audio: ape, apl
		// WavPack *.wv http://en.wikipedia.org/wiki/WavPack
		// mp4 or m4a http://en.wikipedia.org/wiki/MP4, m4b, m4p, 
		// http://en.wikipedia.org/wiki/FFmpeg
		// TODO: Traverse subdirectories; alternate directories
		// ra = RealAudio from RealNetworks (proprietary)
		// CDA, CDDA
		// itunes: itl
		// m3u, m3u8, midi, rmi, wave
		while (!Thread::IsShutdownThreads()) {

			if (stopLoadingListAndGrabNewFilter == LL_RESET) {
				stopLoadingListAndGrabNewFilter = LL_LOADINGFIRSTPAGE;
				loadListLock.Enter();
				// Load all filenames into grid display.  
				// With Debug Version and 67,000 files, it takes about 1700 ms - 330 ms => 1370 ms or 1.3 secs.
				// With Release Version it takes 1020 ms - 256 ms => 770 ms, almost half a second.  Pretty cool. NOTE: FindFile 
				// With background it takes .462 ms to popup (Debug)
				
				flist.Search(CAT << DEFAULT_FILE_DIR << "*" << filter <<"*.*");

				loadListLock.Leave();
				GuiLock __;	// CRITICAL: DO NOT REMOVE! Prevents locking when Clear and the filter action collide somehow		
				activeList.Clear();
				gridRowCount.SetText("..."); // Elipse the row count display so as not to mislead whilest loading
			}

			while (stopLoadingListAndGrabNewFilter == LL_LOADINGPAGE) {
				// CRITICAL: Inner nest allows GuiLock to be freed periodically, allowing user to see keystrokes into the filter list, and lets action flow smoothly
				// CRITICAL: GuiLock prevents freeze
				while (stopLoadingListAndGrabNewFilter == LL_LOADINGPAGE) {
					GuiLock __;	
					if (!flist) {
						// Drop us in to sleep mode
						stopLoadingListAndGrabNewFilter = LL_DONELOADINGPAGE;
						break;
					} else {
						if(flist.IsFile() && PatternMatchMulti(supportedPlaybackTypes, flist.GetName())) {
							activeList.Add(flist.GetName());
						} else {
							// spawn FileList into subdirectory
						}
						flist.Next();
						if(Thread::IsShutdownThreads()) {
							break;
					    }
					}
				}
				
				// Update row count
				gridRowCount.SetText(Format("%C", activeList.GetCount())); // Right-justified (at GUI level).  Apostrophe should put in thousands operator depending on LOCALE. http://www.mkssoftware.com/docs/man1/printf.1.asp
			}

			Sleep(1); // CRITICAL: Prevents app from taking 50% CPU.  Brings it down to 0-1%
		}
	}

	// Load from PostgreSQL 8.4 SONGS_001 table.  Only one to work.  Tried sqlite3 and MySql.
	// We need to lock in a connection so each keystroke in filter does not reconnect.
	//============================================================================================================================
	//
	//  Load Main List From Db
	//
	//  Load main grid on right from the database table songs_001.
	//
	//============================================================================================================================
	void LoadMainListFromDb() {	
		ArrayCtrl &activeList = mainList;
		Sql lst(*mainListSession);
		
		stopLoadingListAndGrabNewFilter = LL_RESET;
		// Don't lock in a transaction, will block other updates
		int gridNewRowNo;
		
		while (!(Thread::IsShutdownThreads()) ) {
			if (stopLoadingListAndGrabNewFilter == LL_KILLLOAD) {
				break;
			}
			
			if (stopLoadingListAndGrabNewFilter == LL_RESET
			 || stopLoadingListAndGrabNewFilter == LL_DONELOADINGPAGE) {
				// if first run we need to set up a DECLARE
				if (stopLoadingListAndGrabNewFilter == LL_RESET) {
					stopLoadingListAndGrabNewFilter = LL_LOADINGFIRSTPAGE;
					#ifdef USE_LOCKS
					loadListLock.Enter(); // This has to be unlocked or the app locks up tight
					#endif
					if (!(lst.Execute("CLOSE songs;"))) {
						LOG(Format("Error in cursor closing: %s", DeQtf(lst.GetLastError())));
					}
			
					lst.Commit(); // No real changes, just close out the transaction
				} else {
					#ifdef USE_LOCKS
					loadListLock.Enter(); // This has to be unlocked or the app locks up tight
					#endif
				}
				
				// if not first run we need to continue fetch and check rowcount return
				if (stopLoadingListAndGrabNewFilter == LL_DONELOADINGPAGE) stopLoadingListAndGrabNewFilter = LL_LOADINGPAGE;
				
				String cmd;				
				if (stopLoadingListAndGrabNewFilter == LL_LOADINGFIRSTPAGE) {
					stopLoadingListAndGrabNewFilter = LL_LOADINGPAGE; // Set it to a generic page
					lst.Begin();

					cmd = "DECLARE songs CURSOR FOR ";
					cmd.Cat() 
					<< " SELECT " 
					<<   SONG_COLUMN_LIST 
					<< " FROM songs_001 s "
					<< " LEFT JOIN list_songs_001 ls ON s.songid = ls.songid "
						;
						
					bool startedWhereClause = false;
					bool startedOrderClause = false;
					
					if (filter.GetLength() > 0)	{
						String lFilter = filter.ToString();
						lFilter = Replace(lFilter, "\\", "\\\\'"); // Prep string so as not to break in the SQL parser: backslash in input string will become an escape char if not escaped
						lFilter = Replace(lFilter, "'", "\\'"); // Prep string so as not to break in the SQL parser: apostrophes must be escaped or the query processor will think input has ended
						// Our FTS!  Uses gin index. (postgre)
						// spaces need to be ignored on ends
						cmd.Cat() << " WHERE s.textsearchable_index_col @@ plainto_tsquery('" << TrimBoth(lFilter.ToString()) << "') ";
						startedWhereClause = true;
					}
					
					// Trying both NOT EXISTS and NOT IN.  NOT EXISTS is fine but a little delay, a rolling look
					// NOT IN is jumpier (better than a rolling), about 2-3 hops down the page
					//cmd.Cat() 
					//	<< (startedWhereClause? " AND " : " WHERE ")
					//	<< " songid NOT IN(SELECT songid FROM list_songs_001)  ";
					startedWhereClause = true;
					
					// If no filter, then randomize return for interests sake
					if (filter.GetLength() == 0) {
						cmd.Cat() << " ORDER BY RANDOM();"; // PostGreSQL Syntax
						startedOrderClause = true;
					// AKartistname_songs_001 (btree) (Has to be ordinal position since using set operation)
					} else {
						cmd.Cat() << " ORDER BY 1;"; // Make sure a single index has all orders and directions and case statement
						startedOrderClause = true;
					}
					LOG(cmd);

					if (!(lst.Execute(cmd))) {
						Exclamation(Format("Error in cursor declaration: %s", DeQtf(lst.GetLastError())));
					}

					#ifdef USE_LOCKS
					loadListLock.Leave(); // Critical. Prevents immediate lockup when the Enter function is double hit.
					#endif
					GuiLock __;	// CRITICAL: DO NOT REMOVE! Prevents locking when Clear and the filter action collide somehow		
					gridRowCount.SetText("..."); // Elipse the row count display so as not to mislead whilest loading

					// Fetch a smaller page
					if (!(lst.Execute("FETCH FORWARD 100 FROM songs"))) {
						Exclamation(Format("Error in first fetch: %s", DeQtf(lst.GetLastError())));
					}
					activeList.Clear(); // Wait till last minute to clear the grid (wish I could disable draw and just logically clear?
				} else {
					// Fetch large pages off screen
				    // Fetch quantity should be small enough to not impact mouse, large enough not to grab thread so frequently that FMOD intermittently exhausts its buffer (noticable)	
					#ifdef USE_LOCKS
					loadListLock.Leave(); // Unlock or app will lock tighter than davey jone's locker
					#endif
					if (!(lst.Execute("FETCH FORWARD 250 FROM songs"))) {
						Exclamation(Format("Error in first fetch: %s", DeQtf(lst.GetLastError())));
					}
					Sleep(1); // Hopefully will give FMOD enough time to prevent buffer exaustion, which it sounded like we were getting
				}

				// Detect end of fetch
				if (lst.GetRowsProcessed() == 0) {
					if (stopLoadingListAndGrabNewFilter != LL_KILLLOAD) 
						stopLoadingListAndGrabNewFilter = LL_DONELOADING;
					if (!(lst.Execute("CLOSE songs;"))) {
						LOG(Format("Error in cursor closing: %s", DeQtf(lst.GetLastError())));
					}
			
					lst.Commit(); // No real changes (yet), just close out the transaction
					GuiLock __;
					gridRowCount.SetText(Format("%C", activeList.GetCount())); // Right-justified (at GUI level).  Apostrophe should put in thousands operator depending on LOCALE. http://www.mkssoftware.com/docs/man1/printf.1.asp
				}
									
				LOG(lst);
			}

			while (stopLoadingListAndGrabNewFilter == LL_LOADINGPAGE) {
				while (stopLoadingListAndGrabNewFilter == LL_LOADINGPAGE) {
					if (!lst.Fetch()) {
						if (stopLoadingListAndGrabNewFilter != LL_KILLLOAD)
							stopLoadingListAndGrabNewFilter = LL_DONELOADINGPAGE;
						GuiLock __;	
						gridRowCount.SetText(Format("%C", activeList.GetCount()));
						break;
					} else {
						GuiLock __;	
						bool inPlayList;
						String inPlayListVal = lst[INPLAYLIST];
						
						activeList.Add();
						gridNewRowNo = activeList.GetCount() - 1;

						// Highlight rows that are in the keeper play list so we can avoid adding similarly named songs twice and know which ones are on the list, and include in search
						
						String filePath = (String)lst[FILEPATH];
						for (int ii = 0; ii < activeList.GetColumnCount(); ii++) {
							if (lst[INPLAYLIST] == "Y") {
								if (IsKnownMissing(filePath)) {
									activeList.SetDisplay(gridNewRowNo, ii, dispRowKeeperMissing); // Italicize keepers that are missing from the file server, so we can replace them
								} else {
									activeList.SetDisplay(gridNewRowNo, ii, dispRowKeeperExists);
								}
							} else {
								if (IsKnownMissing(filePath)) {
									activeList.SetDisplay(gridNewRowNo, ii, dispRowMissing); // In the main list show the missing ones
								} else {
									activeList.SetDisplay(gridNewRowNo, ii, dispRowExists);
								}
							}
						}
						
						int64 songID = lst[SONGID];
						activeList.Set(gridNewRowNo, IDSongID, lst[SONGID]);  // Key when drag-dropping to a playlist in order to link back.
						activeList.Set(gridNewRowNo, IDSongsTbl, lst[SONGS_TBL]); // 2nd element in song key
						activeList.Set(gridNewRowNo, IDTitle, lst[TITLE]);
						activeList.Set(gridNewRowNo, IDFilePath, lst[FILEPATH]); // TODO: Move this to index so invisible
						activeList.Set(gridNewRowNo, IDFileSize, AttrText(lst[FILESIZE].ToString()).Right());
						activeList.Set(gridNewRowNo, IDBitRate, AttrText(lst[BITRATE].ToString()).Right());
						String formattedLength;

						if (!lst[LENGTHMS].IsNull()) {
							int min = (int)lst[LENGTHMS] / (1000 * 60);
							int sec = (int)lst[LENGTHMS] / (1000) % 60;
							formattedLength = Format("%d:%02d", min, sec);
						} else {
							formattedLength = "";
						}
						
						activeList.Set(gridNewRowNo, IDLengthMsStr, AttrText(formattedLength).Right());
						activeList.Set(gridNewRowNo, IDLengthMs, lst[LENGTHMS]); // AttrText not readable, need to use row index
						activeList.Set(gridNewRowNo, IDArtistName, lst[ARTISTNAME]);
						activeList.Set(gridNewRowNo, IDTrackNo, lst[TRACKNO]);
						activeList.Set(gridNewRowNo, IDReleaseYear, lst[RELEASEYEAR]);
						activeList.Set(gridNewRowNo, IDRating, lst[RATING]); // TODO: Convert this to a meaningful display (stars)
						activeList.Set(gridNewRowNo, IDGenreName, lst[GENRENAME]);
						activeList.Set(gridNewRowNo, IDAlbumName, lst[ALBUMNAME]);
						// Strip the double-spacing.  It wastes screen space. Move to mp3 tag updater.
						activeList.Set(gridNewRowNo, IDLyrics, Replace(lst[LYRICS].ToString(), String("\r\n\r\n"), String("\r\n")));
						activeList.Set(gridNewRowNo, IDInPlayList, inPlayList);
						
						if(Thread::IsShutdownThreads()) {
							break;
					    }
					}
				}
			}

			/*lst.Commit();*/ // No transactions! Locks other update activity.

			Sleep(1); // CRITICAL: Prevents app from taking 50% CPU.  Brings it down to 0-1%
			
		}

	}

	//============================================================================================================================
	//
	//  Load Play List From Db
	//
	//============================================================================================================================
	void LoadPlayListFromDb() {	
		ArrayCtrl &activeList = playList;
		String cursorName = "listsongs";
		
		stopLoadingPlayListAndGrabNewFilter = LL_RESET;
		// TODO: Probably needs to be a separate connection per open grid.  Connection pool?
		Sql lst(*playListSession);
		int gridNewRowNo;
		
		while (!Thread::IsShutdownThreads()) {
			if (stopLoadingPlayListAndGrabNewFilter == LL_KILLLOAD)
				break;

			if (stopLoadingPlayListAndGrabNewFilter == LL_RESET
			 || stopLoadingPlayListAndGrabNewFilter == LL_DONELOADINGPAGE) {
				
				if (stopLoadingPlayListAndGrabNewFilter == LL_RESET) {
					stopLoadingPlayListAndGrabNewFilter = LL_LOADINGFIRSTPAGE;

					if (!(lst.Execute(CAT << "CLOSE " << cursorName))) {
						LOG(Format("Error in cursor closing: %s", DeQtf(lst.GetLastError())));
					}
			
					lst.Commit();
				}
				if (stopLoadingPlayListAndGrabNewFilter == LL_DONELOADINGPAGE) 
					stopLoadingPlayListAndGrabNewFilter = LL_LOADINGPAGE;
				loadPlayListLock.Enter();
				
				String cmd;				
				if (stopLoadingPlayListAndGrabNewFilter == LL_LOADINGFIRSTPAGE) {
					stopLoadingPlayListAndGrabNewFilter = LL_LOADINGPAGE;
					lst.Begin();

					cmd = CAT << "DECLARE " << cursorName << " CURSOR FOR "
						" SELECT ls.songid, ls.songs_tbl, ls.looktosongs_tbl, s.title, s.filepath, ls.looktodb_tbl, ls.processorder "
						" , s.filesize, s.artistname, s.albumname, s.trackno "
						" , s.releaseyear, s.rating, s.genrename, s.lyrics, s.bitrate, s.samplerate, s.lengthms "
						" FROM list_songs_001 ls LEFT JOIN songs_001 s ON ls.songid = s.songid AND ls.songs_tbl = s.songs_tbl "
						" WHERE ls.listid = 1 AND ls.listsongs_tbl = 'list_songs_001'" // Just our list
						;					
					// IDEA: Link lists to filter (icon of a chain link)  This will allow you to compare what is on list and what is still outstanding that you might want.
//					if (filter.GetLength() > 0)	{
//						cmd.Cat() << " WHERE textsearchable_index_col @@ plainto_tsquery('" << TrimBoth(filter.ToString()) << "') ";
//					}
					cmd.Cat() << " ORDER BY processorder, title;"; // Nulls first

					if (!(lst.Execute(cmd))) {
						Exclamation(Format("Error in cursor declaration: %s", DeQtf(lst.GetLastError())));
					}

					loadPlayListLock.Leave();
					GuiLock __;
					//gridRowCount.SetText("...");

					if (!(lst.Execute(CAT << "FETCH FORWARD 100 FROM " << cursorName))) {
						Exclamation(Format("Error in first fetch: %s", DeQtf(lst.GetLastError())));
					}
					activeList.Clear();
				} else {
					loadPlayListLock.Leave();

					if (!(lst.Execute(CAT << "FETCH FORWARD 250 FROM " << cursorName))) {
						Exclamation(Format("Error in first fetch: %s", DeQtf(lst.GetLastError())));
					}
					Sleep(1);
				}

				if (lst.GetRowsProcessed() == 0) {
					if (stopLoadingPlayListAndGrabNewFilter != LL_KILLLOAD)
						stopLoadingPlayListAndGrabNewFilter = LL_DONELOADING;
					if (!(lst.Execute(CAT << "CLOSE " << cursorName))) {
						LOG(Format("Error in cursor closing: %s", DeQtf(lst.GetLastError())));
					}
			
					lst.Commit();
					//gridRowCount.SetText(Format("%C", grid.GetCount()));
				}
									
				LOG(lst);
			}

			while (stopLoadingPlayListAndGrabNewFilter == LL_LOADINGPAGE) {
				while (stopLoadingPlayListAndGrabNewFilter == LL_LOADINGPAGE) {
					GuiLock __;	
					if (!lst.Fetch()) {
						if (stopLoadingPlayListAndGrabNewFilter != LL_KILLLOAD)
						stopLoadingPlayListAndGrabNewFilter = LL_DONELOADINGPAGE;
						//gridRowCount.SetText(Format("%C", grid.GetCount()));
						break;
					} else {
						activeList.Add();  // Note: This by itself with mousewheel scrolling and popup info crashes it
						gridNewRowNo = activeList.GetCount() - 1;

						// Shade a row a bit redder if its on the Keeper list, so we know we can still find it on the search side (right)
						
						String filePath = (String)lst[FILEPATH];
						for (int ii = 0; ii < activeList.GetColumnCount(); ii++) {
							if (IsKnownMissing(filePath)) {
								activeList.SetDisplay(gridNewRowNo, ii, dispRowKeeperMissing); // In the main list show the missing ones
							} else {
								activeList.SetDisplay(gridNewRowNo, ii, dispRowKeeperExists);
							}
						}
						int64 songID = lst[SONGID];
						activeList.Set(gridNewRowNo, IDSongID, lst[SONGID]);  // Key when drag-dropping to a playlist in order to link back.
						activeList.Set(gridNewRowNo, IDSongsTbl, lst[SONGS_TBL]); // 2nd element in song key
						activeList.Set(gridNewRowNo, IDTitle, lst[TITLE]);
						activeList.Set(gridNewRowNo, IDFilePath, lst[FILEPATH]);
						activeList.Set(gridNewRowNo, IDFileSize, AttrText(lst[FILESIZE].ToString()).Right());
						activeList.Set(gridNewRowNo, IDBitRate, AttrText(lst[BITRATE].ToString()).Right());
						String formattedLength;

						if (!lst[LENGTHMS].IsNull()) {
							int min = (int)lst[LENGTHMS] / (1000 * 60);
							int sec = (int)lst[LENGTHMS] / (1000) % 60;
							formattedLength = Format("%d:%02d", min, sec);
						} else {
							formattedLength = "";
						}
						
						activeList.Set(gridNewRowNo, IDLengthMs, AttrText(formattedLength).Right());
						activeList.Set(gridNewRowNo, IDArtistName, lst[ARTISTNAME]);
						activeList.Set(gridNewRowNo, IDTrackNo, lst[TRACKNO]);
						activeList.Set(gridNewRowNo, IDReleaseYear, lst[RELEASEYEAR]);
						activeList.Set(gridNewRowNo, IDRating, lst[RATING]);
						activeList.Set(gridNewRowNo, IDGenreName, lst[GENRENAME]);
						activeList.Set(gridNewRowNo, IDAlbumName, lst[ALBUMNAME]);
						activeList.Set(gridNewRowNo, IDLyrics, Replace(lst[LYRICS].ToString(), String("\r\n\r\n"), String("\r\n")));
						// Add add date for sorting
						if(Thread::IsShutdownThreads()) {
							break;
						}
					}
				}
			}

			Sleep(1); // CRITICAL: Prevents app from taking 50% CPU.  Brings it down to 0-1%
		}
	}

	// GenericClick on a song! (Plays on single left click)
	// TODO: This should be a profile option to control single or double click play
	//============================================================================================================================
	//
	//  List Click - User clicked manually on a song, so play it if 
	//
	//============================================================================================================================
	void ListClick(PowerArray *selectedList, int clickCount) {
		activeList = selectedList;
		// Did user enable speed click?
		if (clickCount == 1 && !speedClick.IsChecked()) {
			return;
		}
		
		if (clickCount == 2 && speedClick.IsChecked()) {
			return;
		}

		// Make sure mouse clicked in the grid.  That's all we process for now
		if (!activeList->HasMouse()) {
			//return; // BUG: When popup of long strings happens, grid.HasMouse returns false.
		}
		
		// Check if same as current song playing.  If so, then don't play unless they click again.
		
		Mutex::Lock __(playLock); // Have to lock here since "Sync.." is a shared piece of code, so it would get locked out
		SyncAction(WORKREQ_PLAY, REQSRC_GRID);
	}

	//============================================================================================================================
	//
	//  Remove Song From Playlist
	//
	//============================================================================================================================
	void RemoveSongFromPlayList() {
		if (!playList.IsCursor()) {
			return;
		}
		
		playList.RemoveSelection();	
		SavePlayList();	
	}
	
	//============================================================================================================================
	//
	//  Save Play List
	//
	//  Fetch items from playlist grid and push to db, overwriting contents there
	//
	//============================================================================================================================
	void SavePlayList() {
		Sql update(*playListSession);
		update.Begin();
		WaitCursor wc;
		int processOrder = 0; // Save the order listed on screen to db
		
		// Do some safety checks first
		
		// 1: Count current, count new and pop up a check "are you sure you want to reduce the list from 3000 to 3????"
		
		if (!(update * ::Delete(LIST_SONGS_001)
			.Where(LISTID == dbHandler.listID && LISTS_TBL == dbHandler.listsTableName)))
		{
			update.Rollback();
			LOG("Error deleting: " << update);
		}
		
		playList.Track(false);
		for (int i = 0; i < playList.GetCount(); i++) {

			if (!(update * ::Insert(LIST_SONGS_001)
				(LISTSONGS_TBL, "list_songs_001")
				(LISTS_TBL, dbHandler.listsTableName)
				(SONGID, playList.Get(i, IDSongID))
				(LISTID, dbHandler.listID)
				(SONGS_TBL, playList.Get(i, IDSongsTbl))
				(LISTNAME, dbHandler.listName)
				(NETWORKNAME, dbHandler.networkName)
				(NETWORKS_TBL, dbHandler.networksTableName)
				(MACHINENAME, dbHandler.machineName)
				(MACHINES_TBL, dbHandler.machinesTableName)
				(USERNAME, dbHandler.userName)
				(USERS_TBL, dbHandler.usersTableName)
				(FILEPATH, playList.Get(i, IDFilePath))
				(PROCESSORDER, processOrder)
			))
			{
				update.Rollback();
				LOG("Error adding rows: " << update);
				break;
			}
			
			processOrder++;
		}
		
		//update.Rollback();
		update.Commit();		
		// insert into list_songs_001(listsongs_tbl, lists_tbl, songid, listid, songs_tbl, listname, networkname, machinename, filepath, username) values('list_songs_001', 'lists_001', 1, 76913, 'songs_001', 'Default Play List for JEFF', 'home', 'JEFF-PC',
		//  E'C:\Users\JEFF\Documents\Vuze Downloads\keith jarrett\ecm 1379 keith jarrett - dark intervals\01 opening.mp3', 'JEFF' );

	}

	//============================================================================================================================
	//
	//  Is known to be missing - This file path was lost at some point (bricked drive) but we want to keep the reference to it for now.
	//
	//============================================================================================================================
	bool IsKnownMissing(String &filePath) {
		return 
			filePath.StartsWith("/media/spare320") 
		||
			filePath.StartsWith("/home/jeff/Azureus Downloads")
		;
	}
	//============================================================================================================================
	//
	//  Update FMOD Callback
	//
	//  Polling call from FMOD driver, without which everything locks up.
	//
	//============================================================================================================================
	void UpdateFMODCallback() {
		soundHandler.UpdateFMOD();
	}
	
	// After the user action handlers, we have the conversion to work process
	// This is consolidated to deal with the fact that the same actions are triggerable from different user actions
	// Some are: Batch command line argument, DDE, Keyboard Media Controls, System Shutdown, Mouse on name, control buttons
	//============================================================================================================================
	//
	//  Sync Action
	//
	//============================================================================================================================
	
	void SyncAction(WorkRequestEnum workRequest, WorkReqSourceEnum workReqSource) {
		PowerArray &selectedList = *activeList;
		bool workRequestComplete = false;
		switch (workRequest) {
		case WORKREQ_PLAY:
			{  // Block only need for Mutex, but mutex doesn't work
				// 1. is anything playing?
				// IsPlaying is true even if paused with FMOD
				if (soundHandler.IsPlaying()) {
					// Yes, playing
					// Well, then the requestor must have meant something else besides "play", right?
					
					switch (workReqSource) {
					case REQSRC_APPMEDIABAR:
					case REQSRC_KEYBOARDMEDIACONTROLS:
						// Ah!  They must mean "pause" the currently playing song!
						soundHandler.channel->setPaused(true);  // What if song is already stopped?
						status = CAT << "Paused " << filePath;
						break;
						
					case REQSRC_GRID: 
						// Now, if we clicked on a new song, we mean "Stop playing the current song, play this one instead"
						// But if its the same song, for now we are just going to start it over  (Maybe they meant "Show me detail")
						PlayFromList(selectedList, 0, PLAYPOS_RELATIVE, VERTCURS_DONOT_RECENTER);			
						break;

					default:
						MYERR("Unrecognized workReqSource option, no implementation");
					}
				} else {
					// Song is not playing
					// If its paused, then a song has been selected and loaded (openfile)
					switch (workReqSource) {
					case REQSRC_APPMEDIABAR:
					case REQSRC_KEYBOARDMEDIACONTROLS:
						if (soundHandler.IsPaused()) {
							soundHandler.channel->setPaused(false);
							status = CAT << "Unpaused " << filePath;
						} else {
							// So, not paused, not playing, grab whatever's in the grid
							PlayFromList(selectedList);
						}
						break;
						
					case REQSRC_GRID:
						PlayFromList(selectedList, 0, PLAYPOS_RELATIVE, VERTCURS_DONOT_RECENTER);
						break;
					
					default:
						MYERR("Unrecognized workReqSource option, no implementation");
					} // switch workReqSource

				} // else
	
				SyncControlsToPlayState();
				workRequestComplete = true;
			}
			break;
			
		case WORKREQ_MUTE:
			if (soundHandler.IsMuted()) {
				if (soundHandler.UnMute())
					status = CAT << "Unmuted " << filePath;
				else
					status = CAT << "Already unmuted " << filePath;

			} else {
				if (soundHandler.Mute())
					status = CAT << "Muted " << filePath;
				else
					status = CAT << "Already muted " << filePath;
			}

			SyncControlsToPlayState();
			workRequestComplete = true;
			break;

		case WORKREQ_STOP:
			if (soundHandler.IsPlaying() || soundHandler.IsPaused()) {
				FMODHandler::ignoreCallbackStop++; // Set signal to channel callback so it knows why the last song stops and doesn't start the next song automatically
				soundHandler.channel->stop(); // Should be syncronous
				status = CAT << "Stopped playback " << filePath;
			}

			SyncControlsToPlayState();
			workRequestComplete = true;
			break;

		// Play next could be from the user media controls, or from shuffling on in.  Same action, either way.
		case WORKREQ_PLAYNEXT:
			{
			int tries = 0;
			
			if (IsShuffle()) {
				dword newCursor;
				
				while (tries < 9) {
					newCursor = Random(selectedList.GetCount());
					int genreColNo = selectedList.GetPos(IDGenreName);
					int filePathColNo = selectedList.GetPos(IDFilePath);
					
					String genre = TrimBoth(selectedList.Get(newCursor, genreColNo).ToString());
					String filePath = selectedList.Get(newCursor, filePathColNo);
					
					
					if 
						// Exclude all the crap we hate.  This should be pulled from the db for the current user(s)
						(!PatternMatchMultiNoSpace(
						"Disco;Rap;Books;Electronic;Books & Spoken;Dance;Literature;"
						"Techno;Ska;Euro-Techno;Trip-Hop;Trance;House;Noise;Darkwave;"
						"Eurodance;Dream;Gangsta;Rave;Speech;Porn Groove;Euro-House;Hardcore;"
						"Terror;Anime;Synthpop;Beat;Club-House;Audio Book;Philosophy;Hip-Hop;"
						"Rap & Hip-Hop;Industrial;Hard Rock;Pop Dance", genre)
					&&
						// So sick of Christmas songs in March.
						filePath.Find("Christmas") < 0 
						
					&&
						// Exclude file formats we want to keep for now (not delete) but can't play
						!(filePath.EndsWith(".WMA")) && !(filePath.EndsWith(".wma"))

					&& 
						// Block Swedish songs (Unless it's instrumental?)
						filePath.FindFirstOf("äöåÄ") < 0

					&& 
						// Skip songs we know are not there anymore (based on their drive path)
						!IsKnownMissing(filePath)
					)
					{
						// Ok, song is exceptable
						break;
					} else {
						// No good, try the next random file
						// TODO: Log skipped file for possible review and deletion; flag as skipped in db. (autoskipped, though)
						tries++;
					}
				}
				PlayFromList(selectedList, newCursor, PLAYPOS_ABSOLUTE);
			} else {
				PlayFromList(selectedList, 1); // send array move dir 1 forward
			}
			workRequestComplete = true;
			}
			break;

		case WORKREQ_PLAYPREVIOUS:
			
			// TODO: Is shuffle work backwards? Probably should grab from history
			PlayFromList(selectedList, -1);
			workRequestComplete = true;
			break;

		default:
			MYERR("Unrecognized workRequest option, no implementation");
		}

		// Record for posterity the last request, so mp3 callback can determine next action
		if (workRequestComplete)
			lastWorkRequest.Set(workRequest, workReqSource);
	}

	//============================================================================================================================
	//
	//  Sync Controls to Play State
	//
	//  Update media control buttons on the screen all at once to maintain consistency
	//
	//============================================================================================================================
	void SyncControlsToPlayState() {
		PowerArray &selectedList = *activeList;
		static bool setOnce = false; // For stuff that only needs to be set once;  image loading MAY be noticable enough to delay play/pause; not sure
		if (!setOnce) {
			stopPlaying.Image(MyImages::stopPlaying()); // Buttons use this form
			mute.Image(MyImages::mute());
			playPrevious.Image(MyImages::playPrevious());
			playNext.Image(MyImages::playNext());
		}
		
		if (soundHandler.IsPlaying()) {
			playing.Image(MyImages::pausePlayback()); // flip the image; ButtonOptions use this form
			stopPlaying.Enable(true);
			playing.Check(true); // Set toggle button in raised state
			workState = WORKSTATE_PLAYING;
			
		} else if (soundHandler.IsPaused()) {
			stopPlaying.Enable(true);
			playing.Image(MyImages::playing());
			playing.Check(false); // Not pressed
			workState = WORKSTATE_PAUSED;
			
		} else if (soundHandler.IsStopped()) {
			stopPlaying.Enable(false);
			playing.Image(MyImages::playing());
			playing.Check(false); // Not pressed
			workState = WORKSTATE_NOTHING; // Not necessarily true
		}
		
		// If the user has selected a song, then it makes sense to allow a Next or Previous action
		playNext.Enable(selectedList.IsCursor() && selectedList.GetCursor() < selectedList.GetCount() - 1);
		playPrevious.Enable(selectedList.IsCursor() && selectedList.GetCursor() > 0);
		
		setOnce = true;
	}

	//============================================================================================================================
	//
	//  Sound Handler Callback
	//
	//  actionType - Currently ignored.  FMODHandler::FMODChannelCallback caught FMOD_CHANNEL_CALLBACKTYPE_END and passes a 1, but not used.
	//
	//============================================================================================================================
	static void SoundHandlerCallback(int actionType) {
		if (activePlayer) {
			activePlayer->SyncAction(WORKREQ_PLAYNEXT, REQSRC_SHUFFLEDIN);
		}
	}
	
	//============================================================================================================================
	//
	//  Play From List - Generic, either list.
	//
	//============================================================================================================================
	void PlayFromList(PowerArray &selectedList, int changeAmount = 0, PlayPositionEnum changePositionType = PLAYPOS_RELATIVE, VerticalPositionEnum recenter = VERTCURS_RECENTER) {
		int rowCount = selectedList.GetCount();
		int lastCursor = rowCount - 1;
		if (soundHandler.IsPlaying() || soundHandler.IsPaused())
			FMODHandler::ignoreCallbackStop++; // Here's the deal: If its playing and I start playing another song, a stop callback will occur and must be ignored or it will start jumping through playnext
		
		// Did they select a song and position the array's internal cursor? (row pointer)
		if (!selectedList.IsCursor()) {
			// No, they did not.  Are there any songs?
			if (rowCount > 0) {
				// Yes, there are.  Point to the first one.
				selectedList.SetCursor(0);
			} else {
				return; // No songs listed, nothing to do
			}
		}

		// TODO: Did they click on a playlist song?  We should play that one.
		int targetedCursor;
		
		switch (changePositionType) {
		case PLAYPOS_RELATIVE:
			targetedCursor = selectedList.GetCursor() + changeAmount;
			// Localize any out of range values
			targetedCursor%= rowCount;
			break;
		case PLAYPOS_ABSOLUTE:
			// Localize any out of range values
			targetedCursor = Upp::minmax(changeAmount, 0, lastCursor); // Prevent out of range (log)
			break;
		default:
			Exclamation("Unrecognized changePositionType");
		}
		
		selectedList.SetCursor(targetedCursor); // Positions cursor on screen, scrolls minimum distance
		
		filePath = selectedList.Get(IDFilePath); // Set as member variable since PlayFile uses a Post

		// Play selected song
		
		int rtncode = PlayFile(lastWorkRequest); // Should pass filename :(
		if (rtncode == -1) { // Failed to play, probably a WMA on linux, corrupt, etc.
			activePlayer->SyncAction(WORKREQ_PLAYNEXT, REQSRC_SHUFFLEDIN);
		} else {
			// Recenter active row only if the user didn't click, which is annoying
			if (recenter == VERTCURS_RECENTER) 
				selectedList.CenterCursor();  // We'll see if this is annoying
			
			// While song is playing, update status line
			if (changePositionType == PLAYPOS_RELATIVE) {
				switch (changeAmount) {
				case -1:
					status = CAT << "Playing previous " << filePath;
					break;
				case 1:
					status = CAT << "Playing next " << filePath;
					break;
				case 0:
				default:
					status = CAT << "Playing " << filePath; // 
				}
			} else {
				status = CAT << "Playing " << filePath;
			}

			// If selectedList is main list, the sync playlist if EXACT same song in playlist. Helps when replacing one song with another.	
			if ((PowerArray *)&selectedList == (PowerArray *)&mainList) {
				String title = selectedList.Get(IDTitle);
				String artistName = selectedList.Get(IDArtistName);
				
				// First, try to find the exact instance of the playing file
				int foundIndex = playList.Find(filePath, IDFilePath);
				if (foundIndex >= 0) goto completedStep;
				
				// Next, if title is populated and artist is not, the title probably includes the artist name
				if (!title.IsEmpty() && artistName.IsEmpty()) {
					foundIndex = playList.Find(title, IDTitle);
					if (foundIndex >= 0) goto completedStep;
				}
				
				// Ok, just find the artist
				if (!artistName.IsEmpty()) {
					foundIndex = playList.Find(artistName, IDArtistName);
					if (foundIndex >= 0) {
						playList.Sort(IDArtistName);
						foundIndex = playList.Find(artistName, IDArtistName);
						if (foundIndex >= 0) goto completedStep;
					}
				}

				// back to just title?
				if (!title.IsEmpty()) {
					foundIndex = playList.Find(title, IDTitle);
					if (foundIndex >= 0) goto completedStep;
				}

				// No luck.
				// Ideally scan each column, in the database? Faster in grid
				// Take 20 chars from selected as a window  compare to list in memory, only contains 60 chars of title, 40 of album, 40 of artist (memcmp?
				// Shrink to 19, window across selected, and memcmp
				// Make array 2 fields: first field is 40 char, second field is 1 byte with a "|" in it, to reduce cross-boundary matches.
				// Start with title, artist, then album.  Should be fast.
				// Could rotate some vowels 1 at a time and do over, or trim middle slowly, (search for ever!!!!)  wine=>wyne, wyn  I've=>I have
				// Idea: when adding to array, normalize with dictionary in search array.  and=>&,
				// Try to scroll as many songs onto the visible page for this artist
		completedStep:
				if (foundIndex >= 0) {
					playList.SetCursor(foundIndex);
					playList.CenterCursor();				
				}
			}
		}
	}
	
	// Functions of Work (What this darn class does for a livin!)
	
	// User right-clicked; copy the column value clicked on
	//============================================================================================================================
	void CopyName(PowerArray *selectedList) {
		String selectedItem = selectedList->Get(mainList.GetClickColumn());
		WriteClipboardText(selectedItem);
		status.Set(CAT << "Copied " << selectedItem);
	}

	//============================================================================================================================
	void CopyPath(PowerArray *selectedList) {
		//grid.PasteSelection
		//grid.GetSelectionData()
		//grid.GetSelectCount();
		int count = selectedList->GetSelectCount();
		String selectedItem = selectedList->Get(IDFilePath);
		WriteClipboardText(selectedItem);
		status.Set(CAT << "Copied " << selectedItem);
	}

	//============================================================================================================================
	void GetSongLength(PowerArray *selectedList) {
		String filePath = selectedList->Get(IDFilePath);
		unsigned int length = soundHandler.GetLength(filePath);
		String formattedLength;

		int min = length / (1000 * 60);
		int sec = length / (1000) % 60;
		int msec = length % (10) % 100;
		formattedLength = Format("%d:%02d:%02d", min, sec, msec);

		Exclamation(CAT << "Song is " << length << " ms long, " << formattedLength);
	}
	
	#define CHUNKSIZE 4096 * 8
	
public:
	//============================================================================================================================
	//
	//  Play File Callback - From FMOD
	//
	//============================================================================================================================
	void PlayFileCallback() {
		int rtn = soundHandler.PlayFile(filePath);
		SyncControlsToPlayState();  // Since this is a callback, this is the best time to pick up the most accurate state
	}
	
	//============================================================================================================================
	//
	//  Play File
	//
	//  Tested clients: Play From List
	//
	//============================================================================================================================
	PlayFileEnum PlayFile(LastWorkRequest &lastWorkRequest) {
		if (filePath.GetLength() == 0) {
			MYERR("No file name given");
			return PlayFile_NoFileNameGiven;
		}

		// Check for file existence BEFORE we stop the current file from playing.  I got this behavior from iTunes
		FindFile ff(filePath);
		
		if (!ff) {
			// Convert to Ubuntu's libnotify (libnotify-bin, bzr) notification-daemon 0.4.0 http://library.gnome.org/devel/libnotify/unstable/libnotify-notify.html
			//MYERR(CAT << "File not found. " <<  filePath);
			status = CAT << "Could not find " << filePath;
			// Since we don't actually play a song, we must simulate what a playing song would do, which is send a FMOD_CHANNEL_CALLBACKTYPE_END, which
			// would be caught by the FMODChannelCallback handler, which would then call the handler loaded by InitializeCallback, usually SoundHandlerPlayback, which..
			if (!lastWorkRequest.WasSpecificallyReq()) {
				// If the user clicked on this file manually, we don't play it, it doesn't open.  Maybe beep and set status.
				activePlayer->SyncAction(WORKREQ_PLAYNEXT, REQSRC_SHUFFLEDIN);
			}
			return PlayFile_FileNotFound;
		}
		
		// To significantly increase the user's appearance of responsiveness, push playback to background (wmp3 was MUCH more responsive, but oh well)
		SetTimeCallback(0, THISBACK(PlayFileCallback), TIMERID_PLAYBACKASYNC);
		return PlayFile_Worked; // An assumption
	}
}; // End class MainWin
//============================================================================================================================
//============================================================================================================================
//============================================================================================================================

#endif

/*
  If Upgrade Ultimate++, changes required: (from version 1924)
  - ToolTip.cpp, line 104, function ToolTipHook
    Was: SetTimeCallback(1000, callback(ShowToolTip), (void *)ToolTipHook);
      (Very slow; annoying)
    To:  SetTimeCallback(100, callback(ShowToolTip), (void *)ToolTipHook);
    
  - C:\upp\uppsrc\Core\Core.h(301)
    Was: inline UPP::int64  abs(UPP::int64 x)          { return x < 0 ? -x : x; }
      (Collided with stdlib.h in VS2010)
    To:  inline UPP::int64  abs64(UPP::int64 x)          { return x < 0 ? -x : x; }
    
  - C:\upp\uppsrc\Core\Debug.h(258)
    Was: bool TimingInspector::active = true; 
      (Broke!!!)
    To: bool TimingInspector::active = false;// TURNED THE DAMN THING OFF!
    
    And line 303: added "return;" to skip dump()

  - C:\upp\uppsrc\Core\Core.h(175)
    // fix MSC8 beta problem....
    
#ifdef COMPILER_MSCX
#ifndef PLATFORM_WINCE
namespace std {
	inline void __cdecl _Debug_message(const wchar_t *, const wchar_t *, unsigned int line) {}
};
#endif
#endif

- Bug: Crash during Append in ArrayCtrl under thread with scrolling and popup info active. Fix with Close()
CtrlCore::X11Wnd.cpp
  
void Ctrl::Create0(Ctrl *owner, bool redirect, bool savebits)
{
	GuiLock __;
	ASSERT(IsMainThread());
	LLOG("Create " << Name() << " " << GetRect());
	if (IsChild()) 
	    LLOG("Ctrl::Create0 IsChild = True");
	if (IsOpen()) {
		LLOG("Ctrl::Create0 IsOpen = True");
		Close(); // HACK
	}
	ASSERT(!IsChild() && !IsOpen());

*/
